From 6f0f5a227d8970ba3c8cd3198a759fe3fd5dd697 Mon Sep 17 00:00:00 2001 From: Xinhao Yuan Date: Wed, 19 Aug 2026 14:45:31 -0700 Subject: [PATCH] Move flag reading from worker to runner_utils with some tests. This is to prepare for consolidating the flag reading with runner/sancov libraries. PiperOrigin-RevId: 967420293 --- centipede/BUILD | 9 ++ centipede/engine_worker.cc | 163 ++++++++++++--------------------- centipede/runner_utils.h | 76 +++++++++++++++ centipede/runner_utils_test.cc | 47 ++++++++++ 4 files changed, 189 insertions(+), 106 deletions(-) create mode 100644 centipede/runner_utils_test.cc diff --git a/centipede/BUILD b/centipede/BUILD index dcaabf1c8..2062db85c 100644 --- a/centipede/BUILD +++ b/centipede/BUILD @@ -1730,6 +1730,15 @@ cc_test( ], ) +cc_test( + name = "runner_utils_test", + srcs = ["runner_utils_test.cc"], + deps = [ + ":runner_utils", + "@googletest//:gtest_main", + ], +) + cc_binary( name = "command_test_helper", srcs = ["command_test_helper.cc"], diff --git a/centipede/engine_worker.cc b/centipede/engine_worker.cc index 3c95801fd..80281efec 100644 --- a/centipede/engine_worker.cc +++ b/centipede/engine_worker.cc @@ -99,82 +99,39 @@ inline void WorkerCheck(bool condition, std::string_view error) { std::_Exit(1); } } - -struct WorkerFlags { - bool present; - // length of the flags string, excluding the ending '\0'. - size_t len; - const char* str; -}; +const char* absl_nullable GetWorkerFlagsEnv() { + static const char* flags = []() -> const char* { + // TODO(xinhaoyuan): Rename the env name to FUZZTEST_WORKER_FLAGS. + if (const char* env = std::getenv("CENTIPEDE_RUNNER_FLAGS")) { + WorkerLog("Worker flags: ", env); + char* env_copy = strdup(env); + if (env_copy == nullptr) { + // This should rarely happen. + WorkerLog("Failed to copy the flags env due to allocation failure"); + std::_Exit(1); + } + return env_copy; + } + return nullptr; + }(); + return flags; +} // The first call of this function must be outside of signal handlers since it // allocates memory (enforced by `WorkerInitEarly`). After that it would be // signal-safe. -// -// The worker flags format is `:(NAME=VALUE|SWITCH:)+`. `GetWorkerFlags` -// replaces `:` with '\0' so that we can get null-terminated strings of VALUE -// without copying them, which is important for signal-safety. -const WorkerFlags& GetWorkerFlags() { - static auto worker_flags = []() -> WorkerFlags { - // TODO(xinhaoyuan): Rename the env name to FUZZTEST_WORKER_FLAGS. - const char* env_flags = std::getenv("CENTIPEDE_RUNNER_FLAGS"); - if (env_flags == nullptr) { - return {}; - } - const size_t len = strlen(env_flags); - char* str = reinterpret_cast(malloc(len + 1)); - if (str == nullptr) { - WorkerLog("Cannot allocate the worker flags", LogLnSync{}); +const EngineFlagHelper& GetWorkerFlags() { + static ExplicitLifetime worker_flags; + [[maybe_unused]] static bool construct_once = [] { + worker_flags.Construct(GetWorkerFlagsEnv()); + if (worker_flags->HasAllocationFailure()) { + // This should rarely happen. + WorkerLog("Failed to process the flags due to allocation failure."); std::_Exit(1); } - memcpy(str, env_flags, len); - str[len] = 0; - WorkerLog("Got worker flags ", std::string_view{str, len}, LogLnSync{}); - // 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 < len; ++i) { - if (str[i] == ':') str[i] = 0; - } - return WorkerFlags{true, len, str}; + return true; }(); - return worker_flags; -} - -// `header` should be in the form of `FLAG_NAME=`. -// -// Extracts "value" as a null-terminated string from "\0FLAG_NAME=value\0" in -// the flags. Returns nullptr if it is not found. -const char* GetWorkerFlag(std::string_view header) { - if (header.empty()) return nullptr; - const auto& worker_flags = GetWorkerFlags(); - if (!worker_flags.present) return nullptr; - const auto flags = std::string_view{worker_flags.str, worker_flags.len}; - 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 worker_flags.str + pos + header.size(); - } - pos += header.size(); - } - return nullptr; -} - -// Checks whether "\0{name}\0" exists in the flags. -bool HasWorkerSwitchFlag(std::string_view name) { - if (name.empty()) return false; - const auto& worker_flags = GetWorkerFlags(); - if (!worker_flags.present) return false; - const auto flags = std::string_view{worker_flags.str, worker_flags.len}; - 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; + return *worker_flags; } template @@ -277,8 +234,8 @@ bool WorkerEmitFailureOutput(std::string_view prefix, std::string_view message) { bool ignored = GetWorkerState().has_failure_output.exchange(true); if (!ignored) { - if (const char* failure_description_path = - GetWorkerFlag(kWorkerFailureDescriptionPathFlagHeader); + if (const char* failure_description_path = GetWorkerFlags().GetStringFlag( + kWorkerFailureDescriptionPathFlagHeader); failure_description_path != nullptr) { TrySetFileContents(failure_description_path, /*append=*/false, prefix, message); @@ -322,8 +279,8 @@ void WorkerEmitFinding(std::string_view description, if (!ignored) { WorkerCheck(WorkerEmitFailureOutput(/*prefix=*/"", description), "Failed to emit failure output for the finding"); - if (const char* finding_signature_path = - GetWorkerFlag(kWorkerFailureSignaturePathFlagHeader); + if (const char* finding_signature_path = GetWorkerFlags().GetStringFlag( + kWorkerFailureSignaturePathFlagHeader); finding_signature_path != nullptr) { TrySetFileContents(finding_signature_path, /*append=*/false, signature); @@ -351,7 +308,7 @@ static int persistent_mode_socket; __attribute__((constructor(200))) void WorkerInitEarly() { const char* persistent_mode_socket_path = - GetWorkerFlag(kWorkerPersistentModeSocketPathFlagHeader); + GetWorkerFlags().GetStringFlag(kWorkerPersistentModeSocketPathFlagHeader); if (persistent_mode_socket_path == nullptr) return; persistent_mode_socket = socket(AF_UNIX, SOCK_STREAM, 0); if (persistent_mode_socket < 0) { @@ -409,11 +366,11 @@ __attribute__((constructor(200))) void WorkerInitEarly() { BlobSequence* GetInputsBlobSequence() { static auto result = []() -> BlobSequence* { - if (!HasWorkerSwitchFlag("shmem")) { + if (!GetWorkerFlags().HasSwitchFlag("shmem")) { return nullptr; } const char* input_path = - GetWorkerFlag(kWorkerInputsBlobSequencePathFlagHeader); + GetWorkerFlags().GetStringFlag(kWorkerInputsBlobSequencePathFlagHeader); WorkerCheck(input_path != nullptr, "inputs blob sequence is missing"); return new SharedMemoryBlobSequence(input_path); }(); @@ -422,11 +379,11 @@ BlobSequence* GetInputsBlobSequence() { BlobSequence* GetOutputsBlobSequence() { static auto result = []() -> BlobSequence* { - if (!HasWorkerSwitchFlag("shmem")) { + if (!GetWorkerFlags().HasSwitchFlag("shmem")) { return nullptr; } - const char* output_path = - GetWorkerFlag(kWorkerOutputsBlobSequencePathFlagHeader); + const char* output_path = GetWorkerFlags().GetStringFlag( + kWorkerOutputsBlobSequencePathFlagHeader); WorkerCheck(output_path != nullptr, "outputs blob sequence is missing"); return new SharedMemoryBlobSequence(output_path); }(); @@ -435,12 +392,9 @@ BlobSequence* GetOutputsBlobSequence() { int GetCrossOverLevel() { static int result = []() { - const char* cross_over_level_str = GetWorkerFlag(kWorkerCrossOverLevel); - if (cross_over_level_str != nullptr) { - const int parsed = - atoi(cross_over_level_str); // NOLINT: can't use strto64, etc. - if (0 <= parsed && parsed <= 100) return parsed; - } + const uint64_t cross_over_level = + GetWorkerFlags().GetIntFlag(kWorkerCrossOverLevel, 50); + if (cross_over_level <= 100) return static_cast(cross_over_level); // Default return 50; }(); @@ -449,16 +403,16 @@ int GetCrossOverLevel() { std::optional GetWorkerAction() { static auto worker_action = []() -> std::optional { - if (HasWorkerSwitchFlag("dump_configuration")) { + if (GetWorkerFlags().HasSwitchFlag("dump_configuration")) { return WorkerAction::kNoOp; } - if (HasWorkerSwitchFlag("dump_binary_id")) { + if (GetWorkerFlags().HasSwitchFlag("dump_binary_id")) { return WorkerAction::kGetBinaryId; } - if (HasWorkerSwitchFlag("list_tests")) { + if (GetWorkerFlags().HasSwitchFlag("list_tests")) { return WorkerAction::kListTests; } - if (HasWorkerSwitchFlag("dump_seed_inputs")) { + if (GetWorkerFlags().HasSwitchFlag("dump_seed_inputs")) { return WorkerAction::kTestGetSeeds; } auto* inputs_blobseq = GetInputsBlobSequence(); @@ -499,7 +453,7 @@ FuzzTestInputSink GetInputSinkTo(std::vector& inputs) { void WorkerDoGetBinaryId(const FuzzTestAdapterManager& manager) { if (GetWorkerState().saved_binary_id.exchange(true)) return; const char* binary_id_output_path = - GetWorkerFlag(kWorkerBinaryIdOutputFlagHeader); + GetWorkerFlags().GetStringFlag(kWorkerBinaryIdOutputFlagHeader); WorkerCheck(binary_id_output_path != nullptr, "binary ID output path is not set"); std::vector binary_id; @@ -512,7 +466,7 @@ void WorkerDoGetBinaryId(const FuzzTestAdapterManager& manager) { void WorkerDoListCurrentTest(std::string_view test_name) { const char* test_listing_output_path = - GetWorkerFlag(kWorkerTestListingOutputFlagHeader); + GetWorkerFlags().GetStringFlag(kWorkerTestListingOutputFlagHeader); WorkerCheck(test_listing_output_path != nullptr, "binary ID output path is not set"); TrySetFileContents(test_listing_output_path, @@ -536,7 +490,7 @@ void WorkerDoGetSeeds(const FuzzTestAdapter& adapter) { } static const char* output_dir = - GetWorkerFlag(kWorkerTestGetSeedsOutputDirFlagHeader); + GetWorkerFlags().GetStringFlag(kWorkerTestGetSeedsOutputDirFlagHeader); WorkerCheck(output_dir != nullptr, "seeds output path must be specified"); for (size_t i = 0; i < seed_handles.size(); ++i) { @@ -845,7 +799,7 @@ void WorkerDoExecute(const FuzzTestAdapter& adapter) { const char* FuzzTestWorkerGetTestName() { static auto test_name = []() -> const char* { - return GetWorkerFlag(kWorkerTestNameFlagHeader); + return GetWorkerFlags().GetStringFlag(kWorkerTestNameFlagHeader); }(); return test_name; } @@ -872,15 +826,12 @@ void HandlePersistentMode(const FuzzTestAdapter& adapter) { // to happen when the stdout/stderr are not redirected to a file. (void)ftruncate(fd, 0); } - WorkerLog( - "FuzzTest engine worker (", - req == PersistentModeRequest::kExit ? "exiting persistent mode" - : "persistent mode batch", - "); flags: ", - GetWorkerFlags().present - ? std::string_view{GetWorkerFlags().str, GetWorkerFlags().len} - : "", - LogLnSync{}); + WorkerLog("FuzzTest engine worker (", + req == PersistentModeRequest::kExit ? "exiting persistent mode" + : "persistent mode batch", + "); flags: ", + GetWorkerFlagsEnv() != nullptr ? GetWorkerFlagsEnv() : "", + LogLnSync{}); } if (req == PersistentModeRequest::kExit) break; WorkerCheck(req == PersistentModeRequest::kRunBatch, @@ -917,10 +868,9 @@ void HandlePersistentMode(const FuzzTestAdapter& adapter) { } FuzzTestWorkerStatus WorkerRun(const FuzzTestAdapterManager& manager) { - const auto& flags = GetWorkerFlags(); - WorkerCheck(flags.present, "worker flags must present"); + WorkerCheck(GetWorkerFlagsEnv() != nullptr, "worker flags must present"); - if (HasWorkerSwitchFlag("dump_configuration")) { + if (GetWorkerFlags().HasSwitchFlag("dump_configuration")) { return kFuzzTestWorkerSuccess; } @@ -1018,13 +968,14 @@ FuzzTestWorkerStatus WorkerRun(const FuzzTestAdapterManager& manager) { namespace { using ::fuzztest::internal::GetWorkerFlags; +using ::fuzztest::internal::GetWorkerFlagsEnv; using ::fuzztest::internal::WorkerCheck; using ::fuzztest::internal::WorkerRun; } // namespace int FuzzTestWorkerIsRequired() { - static int result = GetWorkerFlags().present && + static int result = GetWorkerFlagsEnv() != nullptr && fuzztest::internal::GetWorkerAction().has_value(); return result; } diff --git a/centipede/runner_utils.h b/centipede/runner_utils.h index 4b85afa6d..fb28cb594 100644 --- a/centipede/runner_utils.h +++ b/centipede/runner_utils.h @@ -17,9 +17,13 @@ #include +#include #include #include +#include +#include #include +#include #include #include "absl/base/nullability.h" @@ -125,6 +129,78 @@ class ExplicitLifetime { alignas(T) unsigned char space_[sizeof(T)]; }; +// 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; + } + } + + EngineFlagHelper(const EngineFlagHelper&) = delete; + EngineFlagHelper& operator=(const EngineFlagHelper&) = delete; + + ~EngineFlagHelper() { + if (flags_) { + free(flags_); + } + } + + 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; + } + + 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. + } + + 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; + } + + private: + char* absl_nullable flags_; + size_t size_; + bool has_allocation_failure_; +}; + } // namespace fuzztest::internal #endif // THIRD_PARTY_CENTIPEDE_RUNNER_UTILS_H_ diff --git a/centipede/runner_utils_test.cc b/centipede/runner_utils_test.cc new file mode 100644 index 000000000..39367f470 --- /dev/null +++ b/centipede/runner_utils_test.cc @@ -0,0 +1,47 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "./centipede/runner_utils.h" + +#include + +#include "gtest/gtest.h" + +namespace fuzztest::internal { +namespace { + +TEST(RunnerUtilsTest, EngineFlagHelperWorksWithoutFlags) { + EngineFlagHelper helper(nullptr); + EXPECT_FALSE(helper.HasSwitchFlag("foo")); + EXPECT_EQ(helper.GetIntFlag("bar=", 42), 42); + EXPECT_EQ(helper.GetStringFlag("baz="), nullptr); +} + +TEST(RunnerUtilsTest, EngineFlagHelperWorksWithFlags) { + EngineFlagHelper helper(":flag1:flag2=123:str=hello:"); + EXPECT_TRUE(helper.HasSwitchFlag("flag1")); + EXPECT_FALSE(helper.HasSwitchFlag("flag")); + EXPECT_FALSE(helper.HasSwitchFlag("flag1_extra")); + EXPECT_FALSE(helper.HasSwitchFlag("flag2")); + EXPECT_FALSE(helper.HasSwitchFlag("missing")); + + EXPECT_EQ(helper.GetIntFlag("flag2=", 0), 123); + EXPECT_EQ(helper.GetIntFlag("missing=", 999), 999); + + EXPECT_STREQ(helper.GetStringFlag("str="), "hello"); + EXPECT_EQ(helper.GetStringFlag("missing="), nullptr); +} + +} // namespace +} // namespace fuzztest::internal