From 01d3ee5c429abb3138cab1899319fd384063ce40 Mon Sep 17 00:00:00 2001 From: Xinhao Yuan Date: Mon, 6 Jul 2026 08:03:03 -0700 Subject: [PATCH] No public description PiperOrigin-RevId: 943291732 --- centipede/BUILD | 3 + centipede/centipede_test.cc | 40 ++++ centipede/engine_worker.cc | 137 ++++++++++- centipede/runner_utils.cc | 6 + centipede/runner_utils.h | 15 +- centipede/testing/BUILD | 11 + .../testing/test_binary_for_engine_testing.cc | 212 ++++++++++++++++++ 7 files changed, 408 insertions(+), 16 deletions(-) create mode 100644 centipede/testing/test_binary_for_engine_testing.cc diff --git a/centipede/BUILD b/centipede/BUILD index 339229413..607811f8d 100644 --- a/centipede/BUILD +++ b/centipede/BUILD @@ -979,6 +979,7 @@ cc_library( name = "engine_worker", srcs = [ "engine_worker.cc", + "runner_utils.cc", "runner_utils.h", ], hdrs = ["engine_worker_abi.h"], @@ -1956,6 +1957,7 @@ cc_test( timeout = "long", srcs = ["centipede_test.cc"], data = [ + "@com_google_fuzztest//centipede", "@com_google_fuzztest//centipede/testing:abort_fuzz_target", "@com_google_fuzztest//centipede/testing:async_failing_target", "@com_google_fuzztest//centipede/testing:expensive_startup_fuzz_target", @@ -1963,6 +1965,7 @@ cc_test( "@com_google_fuzztest//centipede/testing:fuzz_target_with_custom_mutator", "@com_google_fuzztest//centipede/testing:hanging_fuzz_target", "@com_google_fuzztest//centipede/testing:seeded_fuzz_target", + "@com_google_fuzztest//centipede/testing:test_binary_for_engine_testing", "@com_google_fuzztest//centipede/testing:test_fuzz_target", "@com_google_fuzztest//centipede/testing:test_input_filter", ], diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc index 7a7f1d761..1b1052e94 100644 --- a/centipede/centipede_test.cc +++ b/centipede/centipede_test.cc @@ -1391,5 +1391,45 @@ TEST_F(CentipedeWithTemporaryLocalDir, ToleratesAsyncFailureInMutation) { HasSubstr("Mutate() succeeded"))); } +TEST_F(CentipedeWithTemporaryLocalDir, EngineWorksInWorkerMode) { + TempCorpusDir tmp_dir{test_info_->name()}; + Environment env; + env.workdir = tmp_dir.path(); + env.binary = GetDataDependencyFilepath( + "centipede/testing/test_binary_for_engine_testing"); + env.test_name = "some_test"; + env.populate_binary_info = false; + env.fork_server = false; + env.persistent_mode = true; + env.exit_on_crash = true; + env.stop_at = absl::Now() + absl::Seconds(10); + fuzztest::internal::DefaultCallbacksFactory< + fuzztest::internal::CentipedeDefaultCallbacks> + callbacks; + EXPECT_DEATH( + [&] { std::exit(CentipedeMain(env, callbacks)); }(), + ContainsRegex("Failure *: INPUT FAILURE: some_failure_description")); +} + +TEST_F(CentipedeWithTemporaryLocalDir, EngineWorksInStandaloneMode) { + const std::string centipede_path = + GetDataDependencyFilepath("centipede/centipede"); + const std::string test_binary_path = GetDataDependencyFilepath( + "centipede/testing/test_binary_for_engine_testing"); + // Create a temporary dir and enter it for running the test binary, because + // the test binary uses CWD as the engine workdir. + TempCorpusDir tmp_dir{test_info_->name()}; + const auto test_command = + absl::StrCat("cd ", tmp_dir.path().string(), + " && env FUZZTEST_CENTIPEDE_BINARY_PATH=", centipede_path, + " ", test_binary_path); + EXPECT_DEATH( + [&] { + const int status = std::system(test_command.c_str()); + std::exit(WEXITSTATUS(status)); + }(), + ContainsRegex("Failure *: INPUT FAILURE: some_failure_description")); +} + } // namespace } // namespace fuzztest::internal diff --git a/centipede/engine_worker.cc b/centipede/engine_worker.cc index 01030d1eb..3683e5bf8 100644 --- a/centipede/engine_worker.cc +++ b/centipede/engine_worker.cc @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. #include +#include +#include #include #include @@ -105,7 +107,7 @@ struct WorkerFlags { }; // The first call of this function must be outside of signal handlers since it -// allocates memory (enforced by `GetWorkerFlagsEarly`). After that it would be +// allocates memory (enforced by `WorkerInitEarly`). After that it would be // signal-safe. // // The worker flags format is `:(NAME=VALUE|SWITCH:)+`. `GetWorkerFlags` @@ -137,10 +139,6 @@ const WorkerFlags& GetWorkerFlags() { return worker_flags; } -__attribute__((constructor(200))) void GetWorkerFlagsEarly() { - (void)GetWorkerFlags(); -} - // `header` should be in the form of `FLAG_NAME=`. // // Extracts "value" as a null-terminated string from "\0FLAG_NAME=value\0" in @@ -237,6 +235,9 @@ constexpr std::string_view kWorkerInputsBlobSequencePathFlagHeader = "arg1="; // TODO: Use better flag names when standardizing the protocol. constexpr std::string_view kWorkerOutputsBlobSequencePathFlagHeader = "arg2="; // TODO: Use better flag names when standardizing the protocol. +constexpr std::string_view kWorkerPersistentModeSocketPathFlagHeader = + "persistent_mode_socket="; // TODO: Use better flag names when + // standardizing the protocol. struct WorkerState { std::atomic has_failure_output = false; @@ -323,6 +324,64 @@ inline std::string_view ToStringView(const std::vector& bytes) { return {reinterpret_cast(bytes.data()), bytes.size()}; } +// Zero initialized. +static int persistent_mode_socket; + +__attribute__((constructor(200))) void WorkerInitEarly() { + const char* persistent_mode_socket_path = + GetWorkerFlag(kWorkerPersistentModeSocketPathFlagHeader); + if (persistent_mode_socket_path == nullptr) return; + persistent_mode_socket = socket(AF_UNIX, SOCK_STREAM, 0); + if (persistent_mode_socket < 0) { + WorkerLog( + "Failed to create persistent mode socket - not running persistent " + "mode.", + LogLnSync{}); + return; + } + + struct sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + const size_t socket_path_len = strlen(persistent_mode_socket_path); + WorkerCheck( + socket_path_len < sizeof(addr.sun_path), + "persistent mode socket path string must be fit in sockaddr_un.sun_path"); + std::memcpy(addr.sun_path, persistent_mode_socket_path, socket_path_len); + + int connect_ret = 0; + do { + connect_ret = + connect(persistent_mode_socket, (struct sockaddr*)&addr, sizeof(addr)); + } while (connect_ret == -1 && errno == EINTR); + if (connect_ret == -1) { + WorkerLog("Failed to connect the persistent mode socket to ", + persistent_mode_socket_path, LogLnSync{}); + (void)close(persistent_mode_socket); + persistent_mode_socket = -1; + } + + int flags = fcntl(persistent_mode_socket, F_GETFD); + if (flags == -1) { + WorkerLog( + "fcntl(F_GETFD) failed on the persistent mode socket - exiting " + "persistent mode", + LogLnSync{}); + (void)close(persistent_mode_socket); + persistent_mode_socket = -1; + } + flags |= FD_CLOEXEC; + if (fcntl(persistent_mode_socket, F_SETFD, flags) == -1) { + WorkerLog( + "fcntl(F_SETFD) failed on the persistent mode socket - exiting " + "persistent mode", + LogLnSync{}); + (void)close(persistent_mode_socket); + persistent_mode_socket = -1; + } + WorkerLog("Persistent mode: connected to ", persistent_mode_socket_path, + LogLnSync{}); +} + BlobSequence* GetInputsBlobSequence() { static auto result = []() -> BlobSequence* { if (!HasWorkerSwitchFlag("shmem")) { @@ -695,6 +754,70 @@ const char* FuzzTestWorkerGetTestName() { return test_name; } +void HandlePersistentMode(const FuzzTestAdapter& adapter) { + auto* inputs_blobseq = GetInputsBlobSequence(); + auto* outputs_blobseq = GetOutputsBlobSequence(); + bool first = true; + while (true) { + PersistentModeRequest req; + if (!ReadAll(persistent_mode_socket, reinterpret_cast(&req), 1)) { + WorkerLog("Failed to read request from persistent mode socket: ", + LogErrNo{}, LogLnSync{}); + return; + } + if (first) { + first = false; + WorkerLog("FuzzTest engine worker enter persistent mode", LogLnSync{}); + } else { + // Reset stdout/stderr. + for (int fd = 1; fd <= 2; fd++) { + lseek(fd, 0, SEEK_SET); + // NOTE: Allow ftruncate() to fail by ignoring its return; that's okay + // 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{}); + } + if (req == PersistentModeRequest::kExit) break; + WorkerCheck(req == PersistentModeRequest::kRunBatch, + "Unknown persistent mode request"); + + inputs_blobseq->Reset(); + outputs_blobseq->Reset(); + + // Read the first blob. It indicates what further actions to take. + auto request_type_blob = inputs_blobseq->Read(); + if (IsMutationRequest(request_type_blob)) { + inputs_blobseq->Reset(); + WorkerDoMutate(adapter); + } else if (IsExecutionRequest(request_type_blob)) { + inputs_blobseq->Reset(); + WorkerDoExecute(adapter); + } else { + WorkerCheck(false, "Unknown shmem request"); + } + + const int result = + GetWorkerState().has_finding.load(std::memory_order_relaxed) + ? EXIT_FAILURE + : EXIT_SUCCESS; + if (!WriteAll(persistent_mode_socket, + reinterpret_cast(&result), sizeof(result))) { + WorkerLog("Failed to write response to the persistent mode socket: ", + LogErrNo{}, LogLnSync{}); + return; + } + } +} + FuzzTestWorkerStatus WorkerRun(const FuzzTestAdapterManager& manager) { const auto& flags = GetWorkerFlags(); WorkerCheck(flags.present, "worker flags must present"); @@ -764,7 +887,9 @@ FuzzTestWorkerStatus WorkerRun(const FuzzTestAdapterManager& manager) { WorkerCheck(adapter.FreeInput != nullptr, "FreeInput must be defined"); WorkerCheck(adapter.FreeCtx != nullptr, "FreeCtx must be defined"); - if (action == WorkerAction::kTestGetSeeds) { + if (persistent_mode_socket > 0) { + HandlePersistentMode(adapter); + } else if (action == WorkerAction::kTestGetSeeds) { WorkerDoGetSeeds(adapter); } else if (action == WorkerAction::kTestMutate) { WorkerDoMutate(adapter); diff --git a/centipede/runner_utils.cc b/centipede/runner_utils.cc index 8d1465069..8010574ca 100644 --- a/centipede/runner_utils.cc +++ b/centipede/runner_utils.cc @@ -26,6 +26,12 @@ namespace fuzztest::internal { +// Weak dummy definition of LSan interface in case LSan is missing. +__attribute__((weak)) void __lsan_register_root_region(const void* p, + size_t size) {} +__attribute__((weak)) void __lsan_unregister_root_region(const void* p, + size_t size) {} + void PrintErrorAndExitIf(bool condition, const char* absl_nonnull error) { if (!condition) return; fprintf(stderr, "error: %s\n", error); diff --git a/centipede/runner_utils.h b/centipede/runner_utils.h index 71b300520..1722bfa6f 100644 --- a/centipede/runner_utils.h +++ b/centipede/runner_utils.h @@ -71,10 +71,9 @@ bool ReadAll(int fd, char* data, size_t size); // written, false otherwise due to errors. bool WriteAll(int fd, const char* data, size_t size); -extern "C" void __lsan_register_root_region(const void* p, size_t size) - __attribute__((weak)); -extern "C" void __lsan_unregister_root_region(const void* p, size_t size) - __attribute__((weak)); +// Leak sanitizer interface functions. +extern "C" void __lsan_register_root_region(const void* p, size_t size); +extern "C" void __lsan_unregister_root_region(const void* p, size_t size); // Wraps an object of `T` stored as a plain byte array with explicit // construction/destruction. Needed for runner/dispatcher related global states @@ -107,18 +106,14 @@ class ExplicitLifetime { new (&space_) T(std::forward(args)...); // Needed otherwise lsan may lose track of the pointers inside the object as // it is in-place constructed from the byte array. - if (__lsan_register_root_region) { - __lsan_register_root_region(&space_, sizeof(space_)); - } + __lsan_register_root_region(&space_, sizeof(space_)); } // Destructs the actual object (without reclaiming the space). It can only be // called at most once after recent `Construct()`. void Destruct() { get()->~T(); - if (__lsan_unregister_root_region) { - __lsan_unregister_root_region(&space_, sizeof(space_)); - } + __lsan_unregister_root_region(&space_, sizeof(space_)); } // Gets the pointer to the actual object backed by a plain byte array. Using diff --git a/centipede/testing/BUILD b/centipede/testing/BUILD index e40ae58de..1ddf5e51b 100644 --- a/centipede/testing/BUILD +++ b/centipede/testing/BUILD @@ -497,3 +497,14 @@ sh_test( "@com_google_fuzztest//centipede:test_util_sh", ], ) + +cc_binary( + name = "test_binary_for_engine_testing", + srcs = ["test_binary_for_engine_testing.cc"], + deps = [ + "@abseil-cpp//absl/strings", + "@com_google_fuzztest//centipede:engine_abi", + "@com_google_fuzztest//centipede:engine_controller_with_subprocess", + "@com_google_fuzztest//centipede:engine_worker", + ], +) diff --git a/centipede/testing/test_binary_for_engine_testing.cc b/centipede/testing/test_binary_for_engine_testing.cc new file mode 100644 index 000000000..75c1a42a5 --- /dev/null +++ b/centipede/testing/test_binary_for_engine_testing.cc @@ -0,0 +1,212 @@ +// Copyright 2026 The FuzzTest Authors. +// +// 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 +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "./centipede/engine_abi.h" +#include "./centipede/engine_controller_abi.h" +#include "./centipede/engine_worker_abi.h" + +namespace { + +FuzzTestBytesView ToBytesView(std::string_view sv) { + return {reinterpret_cast(sv.data()), sv.size()}; +} + +void SetUpCoverageDomains(FuzzTestAdapterCtx* ctx, + const FuzzTestCoverageDomainRegistry* registry) { + static constexpr std::string_view kDomainName = "base"; + const FuzzTestCoverageDomain domain = { + /*domain_id=*/0, + /*name=*/ToBytesView(kDomainName), + /*feature_id_bit_size=*/27, + /*counter_bit_size=*/0, + }; + registry->Register(registry->ctx, &domain); +} + +struct AdapterCtx { + const FuzzTestDiagnosticSink* diagnostic_sink; +}; + +struct TestInput { + std::string content; +}; + +void EmitInput(std::string_view input_content, const FuzzTestInputSink* sink) { + sink->Emit(sink->ctx, reinterpret_cast( + new TestInput{std::string{input_content}})); +} + +void GetRandomSeedInput(FuzzTestAdapterCtx* ctx, + const FuzzTestInputSink* sink) { + EmitInput("random_seed", sink); +} + +void Mutate(FuzzTestAdapterCtx* ctx, FuzzTestInputHandle input, int shrink, + const FuzzTestInputSink* sink) { + const auto* input_object = reinterpret_cast(input); + if (input_object->content == "random_seed") { + EmitInput("mutant_1", sink); + } else if (input_object->content == "mutant_1") { + EmitInput("mutant_2", sink); + } else if (input_object->content == "mutant_2") { + EmitInput("mutant_3", sink); + } else { + EmitInput("bad_input", sink); + } +} + +void AddFeature(uint32_t domain, uint32_t feature, uint32_t counter, + std::vector& out) { + out.push_back((static_cast(domain) << 59) | + (static_cast(feature) << 32) | counter); +} + +void Execute(FuzzTestAdapterCtx* ctx, FuzzTestInputHandle input, + const FuzzTestFeedbackSink* sink) { + std::vector features; + auto* adapter_ctx = reinterpret_cast(ctx); + const auto* input_object = reinterpret_cast(input); + if (input_object->content == "random_seed") { + AddFeature(0, 0, 0, features); + AddFeature(0, 1, 0, features); + AddFeature(0, 2, 0, features); + } else if (input_object->content == "mutant_1") { + AddFeature(0, 3, 0, features); + AddFeature(0, 4, 0, features); + } else if (input_object->content == "mutant_2") { + AddFeature(0, 5, 0, features); + } else if (input_object->content == "mutant_3") { + static constexpr std::string_view kDescription = "some_failure_description"; + static constexpr std::string_view kSignature = "some_signature"; + const auto description_view = ToBytesView(kDescription); + const auto signature_view = ToBytesView(kSignature); + adapter_ctx->diagnostic_sink->EmitFinding( + adapter_ctx->diagnostic_sink->ctx, &description_view, &signature_view); + } else { + static constexpr std::string_view kDescription = + "some_other_failure_description"; + static constexpr std::string_view kSignature = "some_other_signature"; + const auto description_view = ToBytesView(kDescription); + const auto signature_view = ToBytesView(kSignature); + adapter_ctx->diagnostic_sink->EmitFinding( + adapter_ctx->diagnostic_sink->ctx, &description_view, &signature_view); + } + const auto features_view = FuzzTestUint64sView{ + features.data(), + features.size(), + }; + sink->EmitCoverageFeatures(sink->ctx, &features_view); +} + +void DeserializeInputContent(FuzzTestAdapterCtx* ctx, + const FuzzTestBytesView* content, + const FuzzTestInputSink* sink) { + auto* input = new TestInput{ + std::string{reinterpret_cast(content->data), content->size}}; + sink->Emit(sink->ctx, reinterpret_cast(input)); +} + +void SerializeInputContent(FuzzTestAdapterCtx* ctx, FuzzTestInputHandle input, + const FuzzTestBytesSink* sink) { + auto* input_object = reinterpret_cast(input); + const FuzzTestBytesView bytes = { + /*data=*/reinterpret_cast(input_object->content.data()), + /*size=*/input_object->content.size(), + }; + sink->Emit(sink->ctx, &bytes); +} + +void FreeInput(FuzzTestAdapterCtx* ctx, FuzzTestInputHandle input) { + delete reinterpret_cast(input); +} + +void FreeCtx(FuzzTestAdapterCtx* ctx) { + delete reinterpret_cast(ctx); +} + +void ConstructAdapter(const FuzzTestDiagnosticSink* sink, + FuzzTestAdapter* adapter_out) { + adapter_out->ctx = + reinterpret_cast(new AdapterCtx{sink}); + adapter_out->SetUpCoverageDomains = SetUpCoverageDomains; + adapter_out->GetRandomSeedInput = GetRandomSeedInput; + adapter_out->Mutate = Mutate; + adapter_out->Execute = Execute; + adapter_out->DeserializeInputContent = DeserializeInputContent; + adapter_out->SerializeInputContent = SerializeInputContent; + adapter_out->FreeInput = FreeInput; + adapter_out->FreeCtx = FreeCtx; +} + +FuzzTestControllerStatus ControllerRun(const FuzzTestAdapterManager* manager, + const std::vector& flags) { + std::vector flags_bytes_view_list; + flags_bytes_view_list.reserve(flags.size()); + for (const auto& flag : flags) { + flags_bytes_view_list.push_back(FuzzTestBytesView{ + /*data=*/reinterpret_cast(flag.data()), + /*size=*/flag.size(), + }); + } + const FuzzTestBytesViews flags_bytes_views = { + /*views=*/flags_bytes_view_list.data(), + /*count=*/flags_bytes_view_list.size(), + }; + return FuzzTestControllerRun(manager, &flags_bytes_views); +} + +} // namespace + +int main(int argc, char** argv) { + FuzzTestAdapterManager manager = { + /*ctx=*/nullptr, + /*GetBinaryId=*/ + [](FuzzTestAdapterManagerCtx* ctx, const FuzzTestBytesSink* sink) { + static constexpr std::string_view kBinaryId = "some_binary_id"; + const auto bytes = ToBytesView(kBinaryId); + sink->Emit(sink->ctx, &bytes); + }, + /*GetTestName=*/ + [](FuzzTestAdapterManagerCtx* ctx, const FuzzTestBytesSink* sink) { + static constexpr std::string_view kTestName = "some_test"; + const auto bytes = ToBytesView(kTestName); + sink->Emit(sink->ctx, &bytes); + }, + /*ConstructAdapter=*/ + [](FuzzTestAdapterManagerCtx* ctx, + const FuzzTestDiagnosticSink* diagnostic_sink, + FuzzTestAdapter* adapter_out) { + ConstructAdapter(diagnostic_sink, adapter_out); + }, + }; + if (const auto worker_status = FuzzTestWorkerMaybeRun(&manager); + worker_status != kFuzzTestWorkerNotRequired) { + return worker_status == kFuzzTestWorkerSuccess ? EXIT_SUCCESS + : EXIT_FAILURE; + } + return ControllerRun(&manager, {absl::StrCat("--binary=", argv[0]), + "--test_name=some_test", + "--populate_binary_info=0", "--fork_server=0", + "--persistent_mode=0", "--exit_on_crash"}) == + kFuzzTestControllerSuccess + ? EXIT_SUCCESS + : EXIT_FAILURE; +}