diff --git a/centipede/binary_info.cc b/centipede/binary_info.cc index 0d02c53eb..1ef77b6cc 100644 --- a/centipede/binary_info.cc +++ b/centipede/binary_info.cc @@ -59,9 +59,11 @@ void BinaryInfo::InitializeFromSanCovBinary( std::filesystem::path{tmp_dir_path} / "binary_info_log_tmp"; FUZZTEST_LOG(INFO) << __func__ << ": tmp_dir: " << tmp_dir; - env_diff.push_back(absl::StrCat( - "CENTIPEDE_RUNNER_FLAGS=:dump_binary_info:arg1=", pc_table_path.path(), - ":arg2=", cf_table_path.path(), ":arg3=", dso_table_path.path(), ":")); + env_diff.push_back( + absl::StrCat("CENTIPEDE_RUNNER_FLAGS=:dump_binary_info:arg1=", + EscapeEngineFlag(pc_table_path.path()), + ":arg2=", EscapeEngineFlag(cf_table_path.path()), + ":arg3=", EscapeEngineFlag(dso_table_path.path()), ":")); Command::Options cmd_options; cmd_options.env_diff = std::move(env_diff); cmd_options.stdout_file_prefix = log_prefix; diff --git a/centipede/centipede_callbacks.cc b/centipede/centipede_callbacks.cc index 961e93535..90c583e20 100644 --- a/centipede/centipede_callbacks.cc +++ b/centipede/centipede_callbacks.cc @@ -360,11 +360,12 @@ std::string CentipedeCallbacks::ConstructRunnerFlags( if (env_.use_dataflow_features) flags.emplace_back("use_dataflow_features"); } if (!env_.runner_dl_path_suffix.empty()) { - flags.emplace_back( - absl::StrCat("dl_path_suffix=", env_.runner_dl_path_suffix)); + flags.emplace_back(absl::StrCat( + "dl_path_suffix=", EscapeEngineFlag(env_.runner_dl_path_suffix))); } if (!env_.pcs_file_path.empty()) - flags.emplace_back(absl::StrCat("pcs_file_path=", env_.pcs_file_path)); + flags.emplace_back( + absl::StrCat("pcs_file_path=", EscapeEngineFlag(env_.pcs_file_path))); if (!extra_flags.empty()) flags.emplace_back(extra_flags); flags.emplace_back(""); return absl::StrJoin(flags, ":"); @@ -399,15 +400,19 @@ CentipedeCallbacks::GetOrCreateCommandContextForBinary( } std::vector env_diff = env_.env_diff_for_binaries; env_diff.push_back(ConstructRunnerFlags( - absl::StrCat(":shmem:test=", env_.test_name, ":arg1=", - inputs_blobseq_.path(), ":arg2=", outputs_blobseq_.path(), - ":failure_description_path=", failure_description_path_, - ":failure_signature_path=", failure_signature_path_, - persistent_mode_server == nullptr - ? "" - : absl::StrCat(":persistent_mode_socket=", - persistent_mode_server->server_path()), - ":"), + absl::StrCat( + ":shmem:test=", EscapeEngineFlag(env_.test_name), + ":arg1=", EscapeEngineFlag(inputs_blobseq_.path()), + ":arg2=", EscapeEngineFlag(outputs_blobseq_.path()), + ":failure_description_path=", + EscapeEngineFlag(failure_description_path_), + ":failure_signature_path=", EscapeEngineFlag(failure_signature_path_), + persistent_mode_server == nullptr + ? "" + : absl::StrCat( + ":persistent_mode_socket=", + EscapeEngineFlag(persistent_mode_server->server_path())), + ":"), disable_coverage)); if (env_.clang_coverage_binary == binary) { @@ -648,12 +653,13 @@ bool CentipedeCallbacks::GetSeedsViaExternalBinary( FUZZTEST_CHECK(!error) << "Failed to create seed inputs directory " << output_dir << ": " << error.message(); - std::string centipede_runner_flags = absl::StrCat( - "CENTIPEDE_RUNNER_FLAGS=:dump_seed_inputs:test=", env_.test_name, - ":arg1=", output_dir.string(), ":"); + std::string centipede_runner_flags = + absl::StrCat("CENTIPEDE_RUNNER_FLAGS=:dump_seed_inputs:test=", + EscapeEngineFlag(env_.test_name), + ":arg1=", EscapeEngineFlag(output_dir.string()), ":"); if (!env_.runner_dl_path_suffix.empty()) { - absl::StrAppend(¢ipede_runner_flags, - "dl_path_suffix=", env_.runner_dl_path_suffix, ":"); + absl::StrAppend(¢ipede_runner_flags, "dl_path_suffix=", + EscapeEngineFlag(env_.runner_dl_path_suffix), ":"); } Command::Options cmd_options; cmd_options.env_diff = env_.env_diff_for_binaries; @@ -716,10 +722,10 @@ bool CentipedeCallbacks::GetSerializedTargetConfigViaExternalBinary( std::filesystem::path{temp_dir_} / "configuration"; std::string centipede_runner_flags = absl::StrCat("CENTIPEDE_RUNNER_FLAGS=:dump_configuration:arg1=", - config_file_path.string(), ":"); + EscapeEngineFlag(config_file_path.string()), ":"); if (!env_.runner_dl_path_suffix.empty()) { - absl::StrAppend(¢ipede_runner_flags, - "dl_path_suffix=", env_.runner_dl_path_suffix, ":"); + absl::StrAppend(¢ipede_runner_flags, "dl_path_suffix=", + EscapeEngineFlag(env_.runner_dl_path_suffix), ":"); } Command::Options cmd_options; cmd_options.env_diff = env_.env_diff_for_binaries; diff --git a/centipede/runner_utils.cc b/centipede/runner_utils.cc index 8010574ca..8007253db 100644 --- a/centipede/runner_utils.cc +++ b/centipede/runner_utils.cc @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include "absl/base/nullability.h" @@ -96,4 +98,83 @@ bool WriteAll(int fd, const char* data, size_t size) { return true; } +size_t ProcessEngineFlags(char* flags, size_t size) { + size_t r = 0; + size_t w = 0; + size_t cur_flag_beg = 0; + for (r = 0; r < size; ++r) { + if (flags[r] == ':') { + if (w > 0 && w == cur_flag_beg) { + // Skip empty flags + continue; + } + flags[w++] = 0; + cur_flag_beg = w; + continue; + } + // Skip copying if no flag beg was scanned before. + if (cur_flag_beg == 0) continue; + if (flags[r] == '\\' && r + 1 < size) { + ++r; + } + flags[w++] = flags[r]; + } + if (cur_flag_beg < 2) return 0; + return cur_flag_beg; +} + +EngineFlagHelper::EngineFlagHelper(const char* absl_nullable flags) + : flags_(nullptr), size_(0), has_allocation_failure_(false) { + if (flags == nullptr) return; + flags_ = strdup(flags); + if (flags_ == nullptr) { + has_allocation_failure_ = true; + return; + } + size_ = ProcessEngineFlags(flags_, strlen(flags_)); +} + +EngineFlagHelper::~EngineFlagHelper() { + if (flags_) { + free(flags_); + } +} + +bool EngineFlagHelper::HasAllocationFailure() const { + return has_allocation_failure_; +} + +bool EngineFlagHelper::HasSwitchFlag(std::string_view flag) const { + return FindEntry(flag, /*match_whole=*/true) != nullptr; +} + +uint64_t EngineFlagHelper::GetIntFlag(std::string_view header, + uint64_t default_value) const { + const char* absl_nullable flag = GetStringFlag(header); + if (flag == nullptr) return default_value; + return atoll(flag); // NOLINT: can't use strto64, etc. +} + +const char* absl_nullable EngineFlagHelper::GetStringFlag( + std::string_view header) const { + const char* absl_nullable entry = FindEntry(header); + if (entry == nullptr) return nullptr; + return entry + header.size(); +} + +const char* absl_nullable EngineFlagHelper::FindEntry(std::string_view flag, + bool match_whole) const { + if (flags_ == nullptr || flag.empty()) return nullptr; + auto flags = std::string_view{flags_, size_}; + while (true) { + auto match = flags.find(flag); + if (match == flags.npos) return nullptr; + if ((match > 0 && flags[match - 1] == 0) && + (!match_whole || flags[match + flag.size()] == 0)) { + return flags.data() + match; + } + flags = flags.substr(match + flag.size()); + } +} + } // namespace fuzztest::internal diff --git a/centipede/runner_utils.h b/centipede/runner_utils.h index fb28cb594..4a4323503 100644 --- a/centipede/runner_utils.h +++ b/centipede/runner_utils.h @@ -129,73 +129,40 @@ class ExplicitLifetime { alignas(T) unsigned char space_[sizeof(T)]; }; +// Processes the `flags` buffer in-place, which comes with the format of +// :(NAME=VALUE:|NAME:)+, where NAME and VALUE can contain escaped chars with +// backslash (\). It drops any chars before the first colon or after the last +// unescaped colon, drops any empty flags, and replaces unescaped colons with +// '\0'. Returns the number of chars in the processed result. +// +// Returns 0 if no proper flags are found in the `flags` buffer. +size_t ProcessEngineFlags(char* flags, size_t size); + // Helper class for processing and reading the engine flags. class EngineFlagHelper { public: // Constructs the helper for a C-string `flags` with the format of :(ENTRY:)+. - explicit EngineFlagHelper(const char* absl_nullable flags) - : flags_(nullptr), size_(0), has_allocation_failure_(false) { - if (flags == nullptr) return; - flags_ = strdup(flags); - if (flags_ == nullptr) { - has_allocation_failure_ = true; - return; - } - size_ = strlen(flags_); - // Post-processing to make '\0' as the separator, making each item as a - // null-terminating string to be used without copying it. - for (size_t i = 0; i < size_; ++i) { - if (flags_[i] == ':') flags_[i] = 0; - } - } + explicit EngineFlagHelper(const char* absl_nullable flags); EngineFlagHelper(const EngineFlagHelper&) = delete; EngineFlagHelper& operator=(const EngineFlagHelper&) = delete; - ~EngineFlagHelper() { - if (flags_) { - free(flags_); - } - } + ~EngineFlagHelper(); - bool HasAllocationFailure() const { return has_allocation_failure_; } - - bool HasSwitchFlag(std::string_view name) const { - if (name.empty() || flags_ == nullptr) return false; - const auto flags = std::string_view{flags_, size_}; - size_t pos = 0; - while (pos = flags.find(name, pos), - pos != flags.npos && pos + name.size() < flags.size()) { - if (pos > 0 && flags[pos - 1] == '\0' && - flags[pos + name.size()] == '\0') { - return true; - } - pos += name.size(); - } - return false; - } + bool HasAllocationFailure() const; - uint64_t GetIntFlag(std::string_view header, uint64_t default_value) const { - const char* absl_nullable flag = GetStringFlag(header); - if (flag == nullptr) return default_value; - return atoll(flag); // NOLINT: can't use strto64, etc. - } + bool HasSwitchFlag(std::string_view flag) const; - const char* absl_nullable GetStringFlag(std::string_view header) const { - if (header.empty() || flags_ == nullptr) return nullptr; - const auto flags = std::string_view{flags_, size_}; - size_t pos = 0; - while (pos = flags.find(header, pos), - pos != flags.npos && pos + header.size() < flags.size()) { - if (pos > 0 && flags[pos - 1] == '\0') { - return flags.data() + pos + header.size(); - } - pos += header.size(); - } - return nullptr; - } + uint64_t GetIntFlag(std::string_view header, uint64_t default_value) const; + + const char* absl_nullable GetStringFlag(std::string_view header) const; private: + // Returns an entry in the flags for `flag`. If `match_whole` is set, match + // `flag` as the whole entry, otherwise match it as a prefix. + const char* absl_nullable FindEntry(std::string_view flag, + bool match_whole = false) const; + char* absl_nullable flags_; size_t size_; bool has_allocation_failure_; diff --git a/centipede/runner_utils_test.cc b/centipede/runner_utils_test.cc index 39367f470..9f8aee1b7 100644 --- a/centipede/runner_utils_test.cc +++ b/centipede/runner_utils_test.cc @@ -14,6 +14,8 @@ #include "./centipede/runner_utils.h" +#include +#include #include #include "gtest/gtest.h" @@ -21,6 +23,141 @@ namespace fuzztest::internal { namespace { +using std::string_view_literals::operator""sv; + +TEST(RunnerUtilsTest, ProcessEngineFlagsEmptyBuffer) { + char buf[] = ""; + EXPECT_EQ(ProcessEngineFlags(buf, 0), 0); +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsNoColons) { + std::string s = "no_colons_here"; + EXPECT_EQ(ProcessEngineFlags(s.data(), s.size()), 0); +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsSingleColon) { + std::string s1 = ":"; + EXPECT_EQ(ProcessEngineFlags(s1.data(), s1.size()), 0); + + std::string s2 = ":only_leading"; + EXPECT_EQ(ProcessEngineFlags(s2.data(), s2.size()), 0); + + std::string s3 = "only_trailing:"; + EXPECT_EQ(ProcessEngineFlags(s3.data(), s3.size()), 0); + + std::string s4 = "middle:only"; + EXPECT_EQ(ProcessEngineFlags(s4.data(), s4.size()), 0); +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsEmptyFlags) { + { + std::string s = "::"; + EXPECT_EQ(ProcessEngineFlags(s.data(), s.size()), 0); + } + { + std::string s = ":::"; + EXPECT_EQ(ProcessEngineFlags(s.data(), s.size()), 0); + } + { + std::string s = "::::"; + EXPECT_EQ(ProcessEngineFlags(s.data(), s.size()), 0); + } + { + std::string s = "::flag::"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0flag\0"sv); + } + { + std::string s = ":::flag1::::flag2:::"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0flag1\0flag2\0"sv); + } +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsSingleFlag) { + { + std::string s = ":flag:"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0flag\0"sv); + } + { + std::string s = ":key=value:"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0key=value\0"sv); + } +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsMultipleFlags) { + std::string s = ":flag1:flag2=val2:flag3:"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0flag1\0flag2=val2\0flag3\0"sv); +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsEscapedColon) { + std::string s = R"(:flag=foo\:bar:flag2:)"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0flag=foo:bar\0flag2\0"sv); +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsEscapedBackslash) { + std::string s = R"(:path=C\:\\dir\\foo\\bar:flag2:)"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), + "\0path=C:\\dir\\foo\\bar\0flag2\0"sv); +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsEscapedOtherChars) { + std::string s = R"(:flag\=name=val\=123:)"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0flag=name=val=123\0"sv); +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsDropsCharsBeforeFirstColon) { + std::string s = "junk_before:flag1:flag2:"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0flag1\0flag2\0"sv); +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsDropsCharsAfterLastColon) { + std::string s = ":flag1:flag2:junk_after"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0flag1\0flag2\0"sv); +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsDropsCharsBeforeAndAfter) { + std::string s = "prefix:flag1=1:flag2=2:suffix"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0flag1=1\0flag2=2\0"sv); +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsTrailingBackslash) { + // Trailing backslash after the last valid colon. + { + std::string s = ":flag:\\"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0flag\0"sv); + } + + // Trailing backslash before colon could close. + { + std::string s = ":flag\\"; + EXPECT_EQ(ProcessEngineFlags(s.data(), s.size()), 0); + } + + // Escaped colon at the end: ":flag\:" means the second colon is escaped, + // so there is no terminating unescaped colon. + { + std::string s = R"(:flag\:)"; + EXPECT_EQ(ProcessEngineFlags(s.data(), s.size()), 0); + } +} + +TEST(RunnerUtilsTest, ProcessEngineFlagsComplex) { + std::string s = R"(ignored:a=1\:2:b=\\c\\:d\=4:done:ignored_too)"; + const size_t len = ProcessEngineFlags(s.data(), s.size()); + EXPECT_EQ(std::string_view(s.data(), len), "\0a=1:2\0b=\\c\\\0d=4\0done\0"sv); +} + TEST(RunnerUtilsTest, EngineFlagHelperWorksWithoutFlags) { EngineFlagHelper helper(nullptr); EXPECT_FALSE(helper.HasSwitchFlag("foo")); @@ -43,5 +180,18 @@ TEST(RunnerUtilsTest, EngineFlagHelperWorksWithFlags) { EXPECT_EQ(helper.GetStringFlag("missing="), nullptr); } +TEST(RunnerUtilsTest, EngineFlagHelperGetsUnescapedFlags) { + EngineFlagHelper helper(R"(:path=C\:\\dir\\foo:flag\=name=val\=123:)"); + EXPECT_STREQ(helper.GetStringFlag("path="), "C:\\dir\\foo"); + EXPECT_STREQ(helper.GetStringFlag("flag=name="), "val=123"); +} + +TEST(RunnerUtilsTest, EngineFlagHelperHandlesEmptyFlags) { + EngineFlagHelper helper(":::flag1::::flag2=123:::"); + EXPECT_TRUE(helper.HasSwitchFlag("flag1")); + EXPECT_EQ(helper.GetIntFlag("flag2=", 0), 123); + EXPECT_FALSE(helper.HasSwitchFlag("")); +} + } // namespace } // namespace fuzztest::internal diff --git a/centipede/util.cc b/centipede/util.cc index ab2eb19ef..583cc1f2c 100644 --- a/centipede/util.cc +++ b/centipede/util.cc @@ -383,4 +383,11 @@ int PollTimeoutMs(absl::Duration timeout) { return static_cast(ms); } +std::string EscapeEngineFlag(std::string_view value) { + return absl::StrReplaceAll(value, { + {":", "\\:"}, + {"\\", "\\\\"}, + }); +} + } // namespace fuzztest::internal diff --git a/centipede/util.h b/centipede/util.h index 4905c68b4..51a40fc20 100644 --- a/centipede/util.h +++ b/centipede/util.h @@ -196,6 +196,9 @@ class MmapNoReserveArray { // Converts `timeout` to an integer value of milliseconds suitable for `poll()`. int PollTimeoutMs(absl::Duration timeout); +// Returns properly escaped `value` to be part of an engine flag. +std::string EscapeEngineFlag(std::string_view value); + } // namespace fuzztest::internal #endif // THIRD_PARTY_CENTIPEDE_UTIL_H_ diff --git a/centipede/util_test.cc b/centipede/util_test.cc index 1629f9212..6f26650ec 100644 --- a/centipede/util_test.cc +++ b/centipede/util_test.cc @@ -304,4 +304,12 @@ TEST(UtilTest, PollTimeoutMsWorks) { EXPECT_GT(PollTimeoutMs(long_finite_duration), 0); } +TEST(UtilTest, EscapeEngineFlagWorks) { + EXPECT_EQ(EscapeEngineFlag(""), ""); + EXPECT_EQ(EscapeEngineFlag("foo"), "foo"); + EXPECT_EQ(EscapeEngineFlag("foo:bar"), "foo\\:bar"); + EXPECT_EQ(EscapeEngineFlag("foo\\bar"), "foo\\\\bar"); + EXPECT_EQ(EscapeEngineFlag("C:\\path:1\\2"), "C\\:\\\\path\\:1\\\\2"); +} + } // namespace fuzztest::internal