diff --git a/centipede/BUILD b/centipede/BUILD index 1000d7f1b..a3424be37 100644 --- a/centipede/BUILD +++ b/centipede/BUILD @@ -974,6 +974,36 @@ cc_library( ], ) +cc_library( + name = "engine_worker", + srcs = [ + "engine_worker.cc", + "runner_utils.h", + ], + hdrs = ["engine_worker_abi.h"], + deps = [ + ":execution_metadata", + ":runner_request", + ":runner_result", + ":shared_memory_blob_sequence", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/random:bit_gen_ref", + "@com_google_fuzztest//centipede:engine_abi", + "@com_google_fuzztest//common:defs", + ], +) + +cc_library( + name = "engine_controller_with_subprocess", + srcs = ["engine_controller_with_subprocess.cc"], + hdrs = ["engine_controller_abi.h"], + deps = [ + "@com_google_fuzztest//centipede:engine_abi", + "@com_google_fuzztest//fuzztest/internal:escaping", + ], +) + # The runner library is special: # * It must not be instrumented with asan, sancov, etc. # * It must not have heavy dependencies, and ideally not at all. diff --git a/centipede/engine_abi.h b/centipede/engine_abi.h index 43e81c741..441c5a141 100644 --- a/centipede/engine_abi.h +++ b/centipede/engine_abi.h @@ -77,7 +77,7 @@ typedef struct { FuzzTestInputSinkCtx* ctx; // Emits a test `input` to the engine. Engine would call - // `FuzzTestAdapter::FreeTestInput` on the emitted input after the engine is + // `FuzzTestAdapter::FreeInput` on the emitted input after the engine is // done with it. void (*Emit)(FuzzTestInputSinkCtx* ctx, FuzzTestInputHandle input); } FuzzTestInputSink; @@ -120,12 +120,13 @@ typedef struct { uint8_t counter_bit_size; } FuzzTestCoverageDomain; -typedef struct FuzzTestDomainRegistryCtx FuzzTestDomainRegistryCtx; +typedef struct FuzzTestCoverageDomainRegistryCtx + FuzzTestCoverageDomainRegistryCtx; typedef struct { - FuzzTestDomainRegistryCtx* ctx; + FuzzTestCoverageDomainRegistryCtx* ctx; // Registers a new coverage `domain`. - void (*Register)(FuzzTestDomainRegistryCtx* ctx, + void (*Register)(FuzzTestCoverageDomainRegistryCtx* ctx, const FuzzTestCoverageDomain* domain); } FuzzTestCoverageDomainRegistry; @@ -134,11 +135,13 @@ typedef struct FuzzTestAdapter { FuzzTestAdapterCtx* ctx; // Registers coverage domains with `registry`. + // The coverage domains to register must be deterministic for the same test of + // the binary. void (*GetCoverageDomains)(FuzzTestAdapterCtx* ctx, const FuzzTestCoverageDomainRegistry* registry); // [Optional] Emits any preset seed inputs of the test using `sink`. - // The output must be deterministic. + // The output must be deterministic for the same test of the binary. void (*GetPresetSeedInputs)(FuzzTestAdapterCtx* ctx, const FuzzTestInputSink* sink); @@ -190,7 +193,7 @@ typedef struct FuzzTestAdapter { FuzzTestInputHandle input); // Drops the ownership of `input` from the engine. - void (*FreeTestInput)(FuzzTestAdapterCtx* ctx, FuzzTestInputHandle input); + void (*FreeInput)(FuzzTestAdapterCtx* ctx, FuzzTestInputHandle input); // Drops the ownership of `ctx` from the engine. void (*FreeCtx)(FuzzTestAdapterCtx* ctx); @@ -216,30 +219,6 @@ typedef struct { FuzzTestAdapter* adapter_out); } FuzzTestAdapterManager; -typedef enum { - kFuzzTestWorkerSuccess = 0, // Test should finish with a success - kFuzzTestWorkerFailure, // Test should finish with a failure. - kFuzzTestWorkerNotRequired, // Test should continue with controller commands. -} FuzzTestWorkerStatus; - -// Try to run as a FuzzTest worker with `manager` if needed. -FuzzTestWorkerStatus FuzzTestWorkerMaybeRun( - const FuzzTestAdapterManager* manager); - -typedef enum { - kFuzzTestControllerSuccess = 0, - kFuzzTestControllerFailure, -} FuzzTestControllerStatus; - -typedef struct { - const FuzzTestBytesView* views; - size_t count; -} FuzzTestBytesViews; - -// Run the FuzzTest controller with `flags` and `manager`. -FuzzTestControllerStatus FuzzTestControllerRun( - const FuzzTestAdapterManager* manager, const FuzzTestBytesViews* flags); - #ifdef __cplusplus } // extern "C" #endif diff --git a/centipede/engine_controller_abi.h b/centipede/engine_controller_abi.h new file mode 100644 index 000000000..74b271435 --- /dev/null +++ b/centipede/engine_controller_abi.h @@ -0,0 +1,46 @@ +// 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. + +#ifndef FUZZTEST_CENTIPEDE_ENGINE_CONTROLLER_ABI_H_ +#define FUZZTEST_CENTIPEDE_ENGINE_CONTROLLER_ABI_H_ + +// FuzzTest engine ABI. +// +// This header needs to be C-compatible. + +#include "./centipede/engine_abi.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + kFuzzTestControllerSuccess = 0, + kFuzzTestControllerFailure, +} FuzzTestControllerStatus; + +typedef struct { + const FuzzTestBytesView* views; + size_t count; +} FuzzTestBytesViews; + +// Run the FuzzTest controller with `flags` and `manager`. +FuzzTestControllerStatus FuzzTestControllerRun( + const FuzzTestAdapterManager* manager, const FuzzTestBytesViews* flags); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // FUZZTEST_CENTIPEDE_ENGINE_CONTROLLER_ABI_H_ diff --git a/centipede/engine_controller_with_subprocess.cc b/centipede/engine_controller_with_subprocess.cc new file mode 100644 index 000000000..b28b73fc3 --- /dev/null +++ b/centipede/engine_controller_with_subprocess.cc @@ -0,0 +1,50 @@ +// 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 "./centipede/engine_abi.h" +#include "./centipede/engine_controller_abi.h" +#include "./fuzztest/internal/escaping.h" + +using fuzztest::internal::ShellEscape; + +FuzzTestControllerStatus FuzzTestControllerRun( + const FuzzTestAdapterManager* manager, const FuzzTestBytesViews* flags) { + // TODO(xinhaoyuan): Use the FuzzTest controller env var later. + static auto centipede_binary_path = []() -> const char* { + const char* env = std::getenv("FUZZTEST_CENTIPEDE_BINARY_PATH"); + if (env == nullptr) return nullptr; + return strdup(env); + }(); + if (centipede_binary_path == nullptr) { + return kFuzzTestControllerFailure; + } + std::string command; + command.append(ShellEscape(centipede_binary_path)); + for (size_t flag_index = 0; flag_index < flags->count; ++flag_index) { + const auto& flag = flags->views[flag_index]; + command.append(" "); + command.append( + ShellEscape({reinterpret_cast(flag.data), flag.size})); + } + int ret = system(command.c_str()); + if (ret == -1) return kFuzzTestControllerFailure; + return WIFEXITED(ret) && WEXITSTATUS(ret) == EXIT_SUCCESS + ? kFuzzTestControllerSuccess + : kFuzzTestControllerFailure; +} diff --git a/centipede/engine_worker.cc b/centipede/engine_worker.cc new file mode 100644 index 000000000..f397c6c02 --- /dev/null +++ b/centipede/engine_worker.cc @@ -0,0 +1,775 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/base/nullability.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/random.h" +#include "./centipede/engine_abi.h" +#include "./centipede/engine_worker_abi.h" +#include "./centipede/execution_metadata.h" +#include "./centipede/runner_request.h" +#include "./centipede/runner_result.h" +#include "./centipede/runner_utils.h" +#include "./centipede/shared_memory_blob_sequence.h" +#include "./common/defs.h" + +namespace fuzztest::internal { + +namespace { + +// Logging needs to be signal-safe and thread-safe. + +struct LogErrNo {}; +struct LogLnSync {}; + +void WorkerLog() {} + +template +void WorkerLog(const T& first, const Rest&... rest) { + if constexpr (std::is_same_v) { + auto saved_errno = errno; + char err_buf[80]; + if (strerror_r(saved_errno, err_buf, sizeof(err_buf)) != 0) { + constexpr std::string_view kFallbackMsg = "[strerror_r failed]"; + static_assert(kFallbackMsg.size() < sizeof(err_buf)); + std::memcpy(err_buf, kFallbackMsg.data(), kFallbackMsg.size()); + err_buf[kFallbackMsg.size()] = 0; + } + WorkerLog(err_buf); + } else if constexpr (std::is_same_v) { + write(STDERR_FILENO, "\n", 1); + fsync(STDERR_FILENO); + } else { + std::string_view sv = first; + while (!sv.empty()) { + const int r = write(STDERR_FILENO, sv.data(), sv.size()); + if (r <= 0) break; + sv = sv.substr(r); + } + } + WorkerLog(rest...); +} + +void WorkerEmitError(std::string_view message); + +inline void WorkerCheck(bool condition, std::string_view error) { + if (!condition) { + WorkerEmitError(error); + std::_Exit(1); + } +} + +#define WORKER_STRINGIFY_EXPANDED(o) #o +#define WORKER_STRINGIFY(o) WORKER_STRINGIFY_EXPANDED(o) +#define WORKER_CHECK_FOR_ERROR() \ + do { \ + InitWorkerState(); \ + WorkerCheck(!worker_state->has_error.load(std::memory_order_relaxed), \ + "Worker must not have error on " __FILE__ \ + ":" WORKER_STRINGIFY(__LINE__)); \ + } while (0) + +struct WorkerFlags { + bool present; + // length of the flags string, excluding the ending '\0'. + size_t len; + const char* str; +}; + +// The first call of this function must be outside of signal handlers since it +// allocates memory (enforced by `GetWorkerFlagsEarly`). After that it would be +// signal-safe. +const WorkerFlags& GetWorkerFlags() { + // Zero-initialized. + static WorkerFlags worker_flags; + [[maybe_unused]] static bool init_worker_flags = []() { + // TODO(xinhaoyuan): Rename the env name to FUZZTEST_WORKER_FLAGS. + const char* env_flags = std::getenv("CENTIPEDE_RUNNER_FLAGS"); + if (env_flags == nullptr) { + return true; + } + worker_flags.len = strlen(env_flags); + char* str = reinterpret_cast(malloc(worker_flags.len + 1)); + if (str == nullptr) { + WorkerLog("Cannot allocate the worker flags", LogLnSync{}); + std::_Exit(1); + } + memcpy(str, env_flags, worker_flags.len); + str[worker_flags.len] = 0; + WorkerLog("Got worker flags ", std::string_view{str, worker_flags.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 < worker_flags.len; ++i) { + if (str[i] == ':') str[i] = 0; + } + worker_flags.str = str; + worker_flags.present = true; + return true; + }(); + return worker_flags; +} + +__attribute__((constructor(200))) void GetWorkerFlagsEarly() { + (void)GetWorkerFlags(); +} + +// `header` should be in the form of `FLAG_NAME=`. +// +// Extracts "value" as a null-terminating string from "\0FLAG_NAME=value\0" in +// the flags. Returns nullptr if it is not found. +const char* GetWorkerFlag(std::string_view header) { + const auto& worker_flags = GetWorkerFlags(); + if (!worker_flags.present) return nullptr; + if (header.empty()) 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) { + const auto& worker_flags = GetWorkerFlags(); + if (!worker_flags.present) return false; + if (name.empty()) 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; +} + +template +void TrySetFileContents(const char* absl_nonnull path, bool append, + C... contents) { + // Needs to be signal-safe. + int f = open(path, O_CREAT | O_WRONLY | (append ? O_APPEND : O_TRUNC), + /*mode=*/0660); + if (f == -1) { + WorkerLog("cannot open path ", path, ": ", LogErrNo{}, LogLnSync{}); + return; + } + ([&] { + std::string_view sv = contents; + while (!sv.empty()) { + const int r = write(f, sv.data(), sv.size()); + if (r < 0) { + WorkerLog("write() failed on ", path, ": ", LogErrNo{}, LogLnSync{}); + return false; + } + if (r == 0) { + WorkerLog("write() on ", path, + " returns 0 unexpectedly. Stopping writing the file."); + return false; + } + sv = sv.substr(r); + } + return true; + }() && + ...); // NOLINT - stop fighting with auto-fomatting. + if (fsync(f) != 0) { + WorkerLog("fsync() failed on ", path, ": ", LogErrNo{}, LogLnSync{}); + } + if (close(f) != 0) { + WorkerLog("close() failed on ", path, ": ", LogErrNo{}, LogLnSync{}); + } +} + +enum class WorkerAction { + kGetBinaryId, + kListTests, + kTestGetSeeds, + kTestMutate, + kTestExecute, +}; + +constexpr char kWorkerBinaryIdOutputFlagHeader[] = "binary_id_output="; +constexpr char kWorkerTestNameFlagHeader[] = "test="; +constexpr char kWorkerTestListingOutputFlagHeader[] = "test_listing_output="; +constexpr char kWorkerTestGetSeedsOutputDirFlagHeader[] = + "arg1="; // TODO: Use better flag names when standardizing the protocol. +constexpr char kWorkerFailureDescriptionPathFlagHeader[] = + "failure_description_path="; +constexpr char kWorkerFailureSignaturePathFlagHeader[] = + "failure_signature_path="; +constexpr char kWorkerInputsBlobSequencePathFlagHeader[] = + "arg1="; // TODO: Use better flag names when standardizing the protocol. +constexpr char kWorkerOutputsBlobSequencePathFlagHeader[] = + "arg2="; // TODO: Use better flag names when standardizing the protocol. + +struct WorkerState { + std::atomic has_failure_output = false; + std::atomic has_error = false; + std::atomic in_adapter_execute = false; + std::atomic has_finding = false; + std::atomic saved_binary_id = false; +}; + +ExplicitLifetime worker_state; + +void InitWorkerState() { + [[maybe_unused]] static bool construct_once = [] { + worker_state.Construct(); + return true; + }(); +} + +bool WorkerEmitFailureOutput(std::string_view prefix, + std::string_view message) { + InitWorkerState(); + bool ignored = worker_state->has_failure_output.exchange(true); + if (!ignored) { + if (const char* failure_description_path = + GetWorkerFlag(kWorkerFailureDescriptionPathFlagHeader); + failure_description_path != nullptr) { + TrySetFileContents(failure_description_path, + /*append=*/false, prefix, message); + } else { + ignored = true; + } + } + if (ignored) { + WorkerLog("Ignored emitting failure output: ", message, LogLnSync{}); + } else { + WorkerLog("Emitted failure output: ", message, LogLnSync{}); + } + return !ignored; +} + +void WorkerEmitError(std::string_view message) { + InitWorkerState(); + worker_state->has_error = true; + WorkerEmitFailureOutput("SETUP FAILURE: ", message); +} + +void WorkerEmitFinding(std::string_view description, + std::string_view signature) { + InitWorkerState(); + WorkerCheck(worker_state->in_adapter_execute.load(std::memory_order_relaxed), + "Must emit finding in adapter execute"); + bool ignored = worker_state->has_finding.exchange(true); + if (!ignored) { + WorkerCheck(WorkerEmitFailureOutput("INPUT FAILURE: ", description), + "Failed to emit failure output for the finding"); + if (const char* finding_signature_path = + GetWorkerFlag(kWorkerFailureSignaturePathFlagHeader); + finding_signature_path != nullptr) { + TrySetFileContents(finding_signature_path, + /*append=*/false, signature); + } + } + + if (ignored) { + WorkerLog("Ignored emitting finding ", description, LogLnSync{}); + } +} + +inline std::string_view ToStringView( + const FuzzTestBytesView* absl_nonnull bytes_view) { + return {reinterpret_cast(bytes_view->data), bytes_view->size}; +} + +inline std::string_view ToStringView(const std::vector& bytes) { + return {reinterpret_cast(bytes.data()), bytes.size()}; +} + +BlobSequence* GetInputsBlobSequence() { + static auto result = []() -> BlobSequence* { + if (!HasWorkerSwitchFlag("shmem")) { + return nullptr; + } + const char* input_path = + GetWorkerFlag(kWorkerInputsBlobSequencePathFlagHeader); + WorkerCheck(input_path != nullptr, "inputs blob sequence is missing"); + return new SharedMemoryBlobSequence(input_path); + }(); + return result; +} + +BlobSequence* GetOutputsBlobSequence() { + static auto result = []() -> BlobSequence* { + if (!HasWorkerSwitchFlag("shmem")) { + return nullptr; + } + const char* output_path = + GetWorkerFlag(kWorkerOutputsBlobSequencePathFlagHeader); + WorkerCheck(output_path != nullptr, "outputs blob sequence is missing"); + return new SharedMemoryBlobSequence(output_path); + }(); + return result; +} + +WorkerAction GetWorkerAction() { + static WorkerAction worker_action = [] { + if (HasWorkerSwitchFlag("dump_binary_id")) { + return WorkerAction::kGetBinaryId; + } + if (HasWorkerSwitchFlag("list_tests")) { + return WorkerAction::kListTests; + } + if (HasWorkerSwitchFlag("dump_seed_inputs")) { + return WorkerAction::kTestGetSeeds; + } + auto* inputs_blobseq = GetInputsBlobSequence(); + WorkerCheck(inputs_blobseq != nullptr, "input blob sequence is not found"); + auto request_type_blob = inputs_blobseq->Read(); + if (IsMutationRequest(request_type_blob)) { + inputs_blobseq->Reset(); + return WorkerAction::kTestMutate; + } + if (IsExecutionRequest(request_type_blob)) { + inputs_blobseq->Reset(); + return WorkerAction::kTestExecute; + } + WorkerCheck(false, "unknown worker action from the flags"); + // should not reach here. + std::abort(); + }(); + return worker_action; +} + +FuzzTestBytesSink GetBytesSinkTo(std::vector& bytes) { + return { + /*ctx=*/reinterpret_cast(&bytes), + /*Emit=*/[](FuzzTestBytesSinkCtx* ctx, const FuzzTestBytesView* view) { + auto* output = reinterpret_cast(ctx); + output->insert(output->end(), view->data, view->data + view->size); + }}; +} + +FuzzTestInputSink GetInputSinkTo(std::vector& inputs) { + return {/*ctx=*/reinterpret_cast(&inputs), + /*Emit=*/[](FuzzTestInputSinkCtx* ctx, FuzzTestInputHandle input) { + auto* output = reinterpret_cast(ctx); + output->push_back(input); + }}; +} + +void WorkerDoGetBinaryId(const FuzzTestAdapterManager& manager) { + InitWorkerState(); + if (worker_state->saved_binary_id.exchange(true)) return; + WORKER_CHECK_FOR_ERROR(); + const char* binary_id_output_path = + GetWorkerFlag(kWorkerBinaryIdOutputFlagHeader); + WorkerCheck(binary_id_output_path != nullptr, + "binary ID output path is not set"); + std::vector binary_id; + const auto sink = GetBytesSinkTo(binary_id); + manager.GetBinaryId(manager.ctx, &sink); + WORKER_CHECK_FOR_ERROR(); + TrySetFileContents(binary_id_output_path, + /*append=*/false, ToStringView(binary_id)); +} + +void WorkerDoListCurrentTest(std::string_view test_name) { + const char* test_listing_output_path = + GetWorkerFlag(kWorkerTestListingOutputFlagHeader); + WorkerCheck(test_listing_output_path != nullptr, + "binary ID output path is not set"); + TrySetFileContents(test_listing_output_path, + /*append=*/true, test_name, "\n"); +} + +void WorkerDoGetSeeds(const FuzzTestAdapter& adapter) { + WorkerCheck(adapter.GetRandomSeedInput != nullptr, + "GetRandomSeedInput is not defined"); + + std::vector seed_handles; + const auto sink = GetInputSinkTo(seed_handles); + if (adapter.GetPresetSeedInputs != nullptr) { + adapter.GetPresetSeedInputs(adapter.ctx, &sink); + WORKER_CHECK_FOR_ERROR(); + } + + // TODO(xinhaoyuan): Make 32 adjustable. + while (seed_handles.size() < 32) { + const size_t prev_size = seed_handles.size(); + adapter.GetRandomSeedInput(adapter.ctx, &sink); + WorkerCheck(seed_handles.size() == prev_size + 1, + "GetRandomSeedInput must emit exact one input"); + } + + static const char* output_dir = + GetWorkerFlag(kWorkerTestGetSeedsOutputDirFlagHeader); + WorkerCheck(output_dir != nullptr, "seeds output path must be specified"); + + for (size_t i = 0; i < seed_handles.size(); ++i) { + char seed_path_buf[PATH_MAX]; + const size_t num_path_chars = + snprintf(seed_path_buf, PATH_MAX, "%s/%09lu", output_dir, i); + WorkerCheck(num_path_chars < PATH_MAX, "seed path reaches PATH_MAX"); + std::vector serialized_input; + const auto sink = GetBytesSinkTo(serialized_input); + adapter.SerializeInputContent(adapter.ctx, seed_handles[i], &sink); + FILE* output_file = fopen(seed_path_buf, "w"); + WorkerCheck(output_file != nullptr, "failed to open the seed file"); + const size_t num_bytes_written = fwrite( + serialized_input.data(), 1, serialized_input.size(), output_file); + WorkerCheck(num_bytes_written == serialized_input.size(), + "wrong number of bytes written for seed"); + fclose(output_file); + adapter.FreeInput(adapter.ctx, seed_handles[i]); + } +} + +absl::BitGenRef GetBitGen() { + static thread_local std::unique_ptr bitgen; + if (bitgen == nullptr) { + bitgen = std::make_unique(); + } + return *bitgen; +} + +void WorkerDoMutate(const FuzzTestAdapter& adapter) { + WorkerCheck(adapter.Mutate != nullptr, "Mutate must be defined"); + + auto* inputs_blobseq = GetInputsBlobSequence(); + auto* outputs_blobseq = GetOutputsBlobSequence(); + WorkerCheck(inputs_blobseq != nullptr && outputs_blobseq != nullptr, + "inputs/outputs blob sequences must be specified"); + + WorkerCheck(MutationResult::WriteHasCustomMutator(true, *outputs_blobseq), + "Failed to write custom mutator indicator!"); + + // Read max_num_mutants. + size_t num_mutants = 0; + size_t num_inputs = 0; + WorkerCheck(IsMutationRequest(inputs_blobseq->Read()), + "Not mutation request!"); + WorkerCheck(IsNumMutants(inputs_blobseq->Read(), num_mutants), + "No num mutants"); + WorkerCheck(IsNumInputs(inputs_blobseq->Read(), num_inputs), "No num inputs"); + + std::vector origin_inputs; + std::vector emitted_inputs; + const FuzzTestInputSink input_sink = { + /*ctx=*/reinterpret_cast(&emitted_inputs), + /*Emit=*/[](FuzzTestInputSinkCtx* ctx, FuzzTestInputHandle input) { + reinterpret_cast(ctx)->push_back(input); + }}; + origin_inputs.reserve(num_inputs); + for (size_t i = 0; i < num_inputs; ++i) { + // If inputs_blobseq have overflown in the engine, we still want to + // handle the first few inputs. + ExecutionMetadata metadata; + if (!IsExecutionMetadata(inputs_blobseq->Read(), metadata)) { + break; + } + auto blob = inputs_blobseq->Read(); + if (!IsDataInput(blob)) break; + emitted_inputs.clear(); + auto input_content = FuzzTestBytesView{blob.data, blob.size}; + auto input_metadata = + FuzzTestBytesView{metadata.cmp_data.data(), metadata.cmp_data.size()}; + adapter.DeserializeInputContent(adapter.ctx, &input_content, &input_sink); + WORKER_CHECK_FOR_ERROR(); + WorkerCheck(emitted_inputs.size() == 1, + "DeserializeInputContent must emit exactly one input"); + if (adapter.UpdateInputMetadata) { + adapter.UpdateInputMetadata(adapter.ctx, &input_metadata, + emitted_inputs[0]); + } + WORKER_CHECK_FOR_ERROR(); + origin_inputs.push_back(emitted_inputs[0]); + } + + if (origin_inputs.empty()) return; + + std::vector mutant_bytes; + const auto mutant_bytes_sink = GetBytesSinkTo(mutant_bytes); + for (size_t i = 0; i < num_mutants; ++i) { + const auto origin = + absl::Uniform(GetBitGen(), 0, origin_inputs.size()); + emitted_inputs.clear(); + adapter.Mutate(adapter.ctx, origin_inputs[origin], /*shrink=*/0, + &input_sink); + WORKER_CHECK_FOR_ERROR(); + WorkerCheck(emitted_inputs.size() == 1, + "Mutate must emit exactly one input"); + mutant_bytes.clear(); + adapter.SerializeInputContent(adapter.ctx, emitted_inputs[0], + &mutant_bytes_sink); + WORKER_CHECK_FOR_ERROR(); + WorkerCheck(MutationResult::WriteMutant(MutantRef{mutant_bytes, origin}, + *outputs_blobseq), + "failed to write mutant"); + adapter.FreeInput(adapter.ctx, emitted_inputs[0]); + WORKER_CHECK_FOR_ERROR(); + } + + for (auto input : origin_inputs) { + adapter.FreeInput(adapter.ctx, input); + WORKER_CHECK_FOR_ERROR(); + } + + return; +} + +constexpr size_t kNumDomains = 32; +struct CoverageDomainConfiguration { + bool registered = false; + std::string name; + uint8_t counter_bits; +}; +std::array coverage_domains; + +void WorkerDoExecute(const FuzzTestAdapter& adapter) { + InitWorkerState(); + WorkerCheck(adapter.Execute != nullptr, "Execute must be defined"); + + auto* inputs_blobseq = GetInputsBlobSequence(); + auto* outputs_blobseq = GetOutputsBlobSequence(); + WorkerCheck(inputs_blobseq != nullptr && outputs_blobseq != nullptr, + "inputs/ouptuts blob sequence must exist"); + + size_t num_inputs = 0; + WorkerCheck(IsExecutionRequest(inputs_blobseq->Read()), + "not an execution request"); + WorkerCheck(IsNumInputs(inputs_blobseq->Read(), num_inputs), + "failed to read num_inputs"); + + [[maybe_unused]] static bool get_coverage_domain = [&] { + FuzzTestCoverageDomainRegistry registry = { + /*ctx=*/nullptr, + /*Register=*/[](FuzzTestCoverageDomainRegistryCtx* ctx, + const FuzzTestCoverageDomain* domain) { + WorkerCheck(domain->domain_id < kNumDomains, + "domain ID is too large"); + WorkerCheck(domain->feature_id_bit_size <= 32, + "domain feature id bit size is too large"); + WorkerCheck(domain->counter_bit_size <= 32, + "domain counter bit size is too large"); + WorkerCheck(!coverage_domains[domain->domain_id].registered, + "domain ID is already registered"); + coverage_domains[domain->domain_id].registered = true; + coverage_domains[domain->domain_id].name = + ToStringView(&domain->name); + coverage_domains[domain->domain_id].counter_bits = + domain->counter_bit_size; + }}; + adapter.GetCoverageDomains(adapter.ctx, ®istry); + return true; + }(); + + // In-loop variables declared outside to save allocations. + std::vector features; + std::vector serialized_metadata; + + for (size_t i = 0; i < num_inputs; i++) { + auto blob = inputs_blobseq->Read(); + if (!blob.IsValid()) return; // no more blobs to read. + WorkerCheck(IsDataInput(blob), "Must read data input"); + + FuzzTestInputHandle input; + FuzzTestInputSink sink = { + /*ctx=*/reinterpret_cast(&input), + /*Emit=*/[](FuzzTestInputSinkCtx* ctx, FuzzTestInputHandle input) { + *reinterpret_cast(ctx) = input; + }}; + + if (!BatchResult::WriteInputBegin(*outputs_blobseq)) { + // TODO: This is to follow the previous behavior, but should we abort + // here? + break; + } + + const auto input_content = FuzzTestBytesView{blob.data, blob.size}; + adapter.DeserializeInputContent(adapter.ctx, &input_content, &sink); + WORKER_CHECK_FOR_ERROR(); + + features.clear(); + FuzzTestFeedbackSink feedback_sink = { + /*ctx=*/reinterpret_cast(&features), + /*EmitFeatures=*/[](FuzzTestFeedbackSinkCtx* ctx, + const FuzzTestUint64sView* features) { + auto* output = reinterpret_cast*>(ctx); + output->insert(output->end(), features->data, + features->data + features->size); + }}; + + worker_state->in_adapter_execute = true; + adapter.Execute(adapter.ctx, input, &feedback_sink); + worker_state->in_adapter_execute = false; + WORKER_CHECK_FOR_ERROR(); + + serialized_metadata.clear(); + if (adapter.SerializeInputMetadata) { + const auto metadata_sink = GetBytesSinkTo(serialized_metadata); + adapter.SerializeInputMetadata(adapter.ctx, input, &metadata_sink); + } + adapter.FreeInput(adapter.ctx, input); + WORKER_CHECK_FOR_ERROR(); + + if (worker_state->has_finding.load(std::memory_order_relaxed)) return; + + // Convert to the Centipede feature layout with possible loss. + for (auto& feature : features) { + const uint64_t domain_id = feature >> 59; + WorkerCheck(coverage_domains[domain_id].registered, + "Emitted features in unregistered domain"); + if (coverage_domains[domain_id].counter_bits > 0) { + const uint64_t feature_id = (feature >> 32) & 0x001ffffful; + const uint64_t counter = + (feature >> + std::max(0, static_cast( + coverage_domains[domain_id].counter_bits - 6))) & + 0x3f; + feature = (domain_id << 27) | (feature_id << 6) | counter; + } else { + feature = feature >> 32; + } + } + + WorkerCheck(BatchResult::WriteOneFeatureVec( + features.data(), features.size(), *outputs_blobseq), + "failed to write feedback"); + WorkerCheck( + BatchResult::WriteMetadata(serialized_metadata, *outputs_blobseq), + "failed to write metadata"); + if (!BatchResult::WriteInputEnd(*outputs_blobseq)) { + // TODO: This is to follow the previous behavior, but should we abort + // here? + break; + } + } +} + +const char* FuzzTestWorkerGetTestName() { + static auto test_name = []() -> const char* { + return GetWorkerFlag(kWorkerTestNameFlagHeader); + }(); + return test_name; +} + +FuzzTestWorkerStatus WorkerMaybeRun(const FuzzTestAdapterManager& manager) { + InitWorkerState(); + const auto& flags = GetWorkerFlags(); + if (!flags.present) return kFuzzTestWorkerNotRequired; + + if (HasWorkerSwitchFlag("dump_configuration")) { + return kFuzzTestWorkerSuccess; + } + + const auto action = GetWorkerAction(); + if (action == WorkerAction::kGetBinaryId) { + WorkerDoGetBinaryId(manager); + return kFuzzTestWorkerSuccess; + } + + WorkerCheck(manager.GetTestName != nullptr, "GetTestName is not defined"); + + std::vector test_name; + const auto sink = GetBytesSinkTo(test_name); + manager.GetTestName(manager.ctx, &sink); + WORKER_CHECK_FOR_ERROR(); + + if (action == WorkerAction::kListTests) { + WorkerDoListCurrentTest(ToStringView(test_name)); + return kFuzzTestWorkerSuccess; + } + + const char* worker_test_name = FuzzTestWorkerGetTestName(); + WorkerCheck(worker_test_name != nullptr, + "Worker requested test name must not be empty"); + if (ToStringView(test_name) != worker_test_name) { + return kFuzzTestWorkerSuccess; + } + + static const FuzzTestDiagnosticSink diagnostic_sink = { + /*ctx=*/nullptr, + /*EmitError=*/ + [](FuzzTestDiagnosticSinkCtx* ctx, const FuzzTestBytesView* message) { + WorkerEmitError(ToStringView(message)); + }, + /*EmitWarning=*/ + [](FuzzTestDiagnosticSinkCtx* ctx, const FuzzTestBytesView* message) { + // TODO(xinhaoyuan): Emit the warning to the engine. + WorkerLog("Got warning ", ToStringView(message)); + }, + /*EmitFinding=*/ + [](FuzzTestDiagnosticSinkCtx* ctx, const FuzzTestBytesView* description, + const FuzzTestBytesView* signature) { + WorkerEmitFinding(ToStringView(description), ToStringView(signature)); + }, + }; + WorkerCheck(manager.ConstructAdapter != nullptr, + "ConstructAdapter is not defined"); + FuzzTestAdapter adapter = {}; + manager.ConstructAdapter(manager.ctx, /*diagnostic_sink=*/&diagnostic_sink, + &adapter); + WORKER_CHECK_FOR_ERROR(); + WorkerCheck(adapter.SerializeInputContent != nullptr, + "SerializeInputContent must be defined"); + WorkerCheck(adapter.DeserializeInputContent != nullptr, + "DeserializeInputContent must be defined"); + WorkerCheck(adapter.FreeInput != nullptr, "FreeInput must be defined"); + WorkerCheck(adapter.FreeCtx != nullptr, "FreeCtx must be defined"); + + if (action == WorkerAction::kTestGetSeeds) { + WorkerDoGetSeeds(adapter); + } else if (action == WorkerAction::kTestMutate) { + WorkerDoMutate(adapter); + } else if (action == WorkerAction::kTestExecute) { + WorkerDoExecute(adapter); + } else { + WorkerCheck(false, "unknown worker action to take"); + } + + adapter.FreeCtx(adapter.ctx); // NOLINT + WORKER_CHECK_FOR_ERROR(); + + return worker_state->has_finding.load(std::memory_order_relaxed) + ? kFuzzTestWorkerFailure + : kFuzzTestWorkerSuccess; +} + +} // namespace + +} // namespace fuzztest::internal + +using ::fuzztest::internal::WorkerCheck; +using ::fuzztest::internal::WorkerMaybeRun; + +FuzzTestWorkerStatus FuzzTestWorkerMaybeRun( + const FuzzTestAdapterManager* manager) { + WorkerCheck(manager != nullptr, "manager must not be nullptr"); + return WorkerMaybeRun(*manager); +} diff --git a/centipede/engine_worker_abi.h b/centipede/engine_worker_abi.h new file mode 100644 index 000000000..e43574242 --- /dev/null +++ b/centipede/engine_worker_abi.h @@ -0,0 +1,45 @@ +// 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. + +#ifndef FUZZTEST_CENTIPEDE_ENGINE_WORKER_ABI_H_ +#define FUZZTEST_CENTIPEDE_ENGINE_WORKER_ABI_H_ + +// FuzzTest engine worker ABI. +// +// This header needs to be C-compatible. + +#include +#include + +#include "./centipede/engine_abi.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + kFuzzTestWorkerSuccess = 0, // Test should finish with a success + kFuzzTestWorkerFailure, // Test should finish with a failure. + kFuzzTestWorkerNotRequired, // Test should continue with controller commands. +} FuzzTestWorkerStatus; + +// Try to run as a FuzzTest worker with `manager` if needed. +FuzzTestWorkerStatus FuzzTestWorkerMaybeRun( + const FuzzTestAdapterManager* manager); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // FUZZTEST_CENTIPEDE_ENGINE_WORKER_ABI_H_ diff --git a/centipede/testing/BUILD b/centipede/testing/BUILD index e40ae58de..e53d3e260 100644 --- a/centipede/testing/BUILD +++ b/centipede/testing/BUILD @@ -366,6 +366,17 @@ sh_test( ], ) +sh_test( + name = "engine_test", + srcs = ["engine_test.sh"], + data = [ + ":engine_test_binary", + "@com_google_fuzztest//centipede", + "@com_google_fuzztest//centipede:test_fuzzing_util_sh", + "@com_google_fuzztest//centipede:test_util_sh", + ], +) + sh_test( name = "runner_test", srcs = ["runner_test.sh"], @@ -497,3 +508,14 @@ sh_test( "@com_google_fuzztest//centipede:test_util_sh", ], ) + +cc_binary( + name = "engine_test_binary", + srcs = ["engine_test_binary.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/engine_test.sh b/centipede/testing/engine_test.sh new file mode 100755 index 000000000..916cc41f3 --- /dev/null +++ b/centipede/testing/engine_test.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# 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. + +# Test running engine_test_binary with Centipede. + +set -eu + +source "$(dirname "$0")/../test_fuzzing_util.sh" +source "$(dirname "$0")/../test_util.sh" + +CENTIPEDE_TEST_SRCDIR="$(fuzztest::internal::get_centipede_test_srcdir)" + +fuzztest::internal::maybe_set_var_to_executable_path \ + CENTIPEDE_BINARY "${CENTIPEDE_TEST_SRCDIR}/centipede" +fuzztest::internal::maybe_set_var_to_executable_path \ + ENGINE_TEST_BINARY "${CENTIPEDE_TEST_SRCDIR}/testing/engine_test_binary" + +# --- Test 1: Run centipede directly with engine_test_binary as target --- +echo "============ Running Test 1: Centipede -> engine_test_binary" + +FUNC1="test_engine_direct" +WD1="${TEST_TMPDIR}/${FUNC1}/WD" +LOG1="${TEST_TMPDIR}/${FUNC1}/log" +fuzztest::internal::ensure_empty_dir "${WD1}" + +set +e +"${CENTIPEDE_BINARY}" \ + --binary="${ENGINE_TEST_BINARY}" \ + --workdir="${WD1}" \ + --test_name="some_test" \ + --populate_binary_info=0 \ + --fork_server=0 \ + --persistent_mode=0 \ + --exit_on_crash \ + --symbolizer_path=/dev/null \ + > "${LOG1}" 2>&1 +RC1=$? +set -e + +cat "${LOG1}" + +if [ $RC1 -eq 0 ]; then + echo "Test 1 failed: Centipede exited with 0, expected non-zero exit code on crash" + exit 1 +fi + +fuzztest::internal::assert_regex_in_file "Failure.*: some_failure_description" "${LOG1}" +echo "Test 1 PASSED" + +# --- Test 2: Run engine_test_binary directly with FUZZTEST_CENTIPEDE_BINARY_PATH --- +echo "============ Running Test 2: engine_test_binary (controller) -> Centipede -> engine_test_binary (worker)" + +FUNC2="test_engine_via_env" +WD2="${TEST_TMPDIR}/${FUNC2}/WD" +LOG2="${TEST_TMPDIR}/${FUNC2}/log" +fuzztest::internal::ensure_empty_dir "${WD2}" + +# Since we cannot pass --workdir to the controller easily (it hardcodes flags), +# we run it in a temporary directory so that default workdir (if any) is created there. +# We must set FUZZTEST_CENTIPEDE_BINARY_PATH when running ENGINE_TEST_BINARY. +( + cd "${WD2}" + set +e + FUZZTEST_CENTIPEDE_BINARY_PATH="${CENTIPEDE_BINARY}" "${ENGINE_TEST_BINARY}" > "${LOG2}" 2>&1 + RC2=$? + set -e + + cat "${LOG2}" + + if [ $RC2 -eq 0 ]; then + echo "Test 2 failed: ENGINE_TEST_BINARY exited with 0, expected non-zero exit code on crash" + exit 1 + fi + + # The output of Centipede should be forwarded to LOG2 by system(). + fuzztest::internal::assert_regex_in_file "Failure.*: some_failure_description" "${LOG2}" +) +RC_SUB=$? + +if [ $RC_SUB -ne 0 ]; then + echo "Test 2 failed" + exit 1 +fi + +echo "Test 2 PASSED" +echo "ALL TESTS PASSED" diff --git a/centipede/testing/engine_test_binary.cc b/centipede/testing/engine_test_binary.cc new file mode 100644 index 000000000..285f7a2e2 --- /dev/null +++ b/centipede/testing/engine_test_binary.cc @@ -0,0 +1,232 @@ +// 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 + +#include "absl/strings/str_cat.h" +#include "./centipede/engine_abi.h" +#include "./centipede/engine_controller_abi.h" +#include "./centipede/engine_worker_abi.h" + +namespace { + +std::string_view GetWorkerTestParam() { + static auto worker_test_param_env = []() -> const char* { + const char* env = getenv("FUZZTEST_WORKER_TEST_PARAM"); + if (!env) return nullptr; + return strdup(env); + }(); + static auto worker_test_param = []() -> std::string_view { + return worker_test_param_env == nullptr ? "" : worker_test_param_env; + }(); + return worker_test_param; +} + +FuzzTestBytesView ToBytesView(std::string_view sv) { + return {reinterpret_cast(sv.data()), sv.size()}; +} + +constexpr std::string_view kDomainName = "base"; +void GetCoverageDomains(FuzzTestAdapterCtx* ctx, + const FuzzTestCoverageDomainRegistry* registry) { + 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 description = "some_failure_description"; + static constexpr std::string_view signature = "some_signature"; + const auto description_view = ToBytesView(description); + const auto signature_view = ToBytesView(signature); + adapter_ctx->diagnostic_sink->EmitFinding( + adapter_ctx->diagnostic_sink->ctx, &description_view, &signature_view); + } else { + static constexpr std::string_view description = + "some_other_failure_description"; + static constexpr std::string_view signature = "some_other_signature"; + const auto description_view = ToBytesView(description); + const auto signature_view = ToBytesView(signature); + adapter_ctx->diagnostic_sink->EmitFinding( + adapter_ctx->diagnostic_sink->ctx, &description_view, &signature_view); + } + const auto features_view = FuzzTestUint64sView{ + /*data=*/features.data(), + /*size=*/features.size(), + }; + sink->EmitCoverageFeatures(sink->ctx, &features_view); +} + +void DeserializeInputContent(FuzzTestAdapterCtx* ctx, + const FuzzTestBytesView* content, + const FuzzTestInputSink* sink) { + auto* input = new TestInput{}; + input->content.resize(content->size); + std::memcpy(input->content.data(), 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->GetCoverageDomains = GetCoverageDomains; + 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 binary_id = "some_binary_id"; + const auto bytes = ToBytesView(binary_id); + sink->Emit(sink->ctx, &bytes); + }, + /*GetTestName=*/ + [](FuzzTestAdapterManagerCtx* ctx, const FuzzTestBytesSink* sink) { + static constexpr std::string_view test_name = "some_test"; + const auto bytes = ToBytesView(test_name); + sink->Emit(sink->ctx, &bytes); + }, + /*ConstructAdapter=*/ + [](FuzzTestAdapterManagerCtx* ctx, + const FuzzTestDiagnosticSink* diagnostic_sink, + FuzzTestAdapter* adapter_out) { + if (GetWorkerTestParam() == "error_on_construct_adapter") { + static constexpr std::string_view error = "some error"; + const auto error_bytes = ToBytesView(error); + diagnostic_sink->EmitError(diagnostic_sink->ctx, &error_bytes); + return; + } + 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; +}