Basic dispatcher prototype. Tests will be followed later. PiperOrigin-RevId: 772497181
diff --git a/centipede/BUILD b/centipede/BUILD index 9c43a21..6366036 100644 --- a/centipede/BUILD +++ b/centipede/BUILD
@@ -982,6 +982,20 @@ # This has no dependency and only uses string, pair and vector. ) +cc_library( + name = "dispatcher", + srcs = ["dispatcher.cc"], + hdrs = ["dispatcher.h"], + deps = [ + ":execution_metadata", + ":runner_request", + ":runner_result", + ":shared_memory_blob_sequence", + "@abseil-cpp//absl/base:nullability", + "@com_google_fuzztest//common:defs", + ], +) + # 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/dispatcher.cc b/centipede/dispatcher.cc new file mode 100644 index 0000000..45e8c6a --- /dev/null +++ b/centipede/dispatcher.cc
@@ -0,0 +1,594 @@ +// Copyright 2025 The Centipede 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 "./centipede/dispatcher.h" + +#include <fcntl.h> +#include <unistd.h> + +#include <atomic> +#include <cerrno> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +#include "absl/base/nullability.h" +#include "./centipede/execution_metadata.h" +#include "./centipede/runner_request.h" +#include "./centipede/runner_result.h" +#include "./centipede/shared_memory_blob_sequence.h" +#include "./common/defs.h" + +namespace fuzztest::internal { + +namespace { + +// Logging needs to be signal safe. + +struct LogErrNo {}; +struct LogLnSync {}; + +void DispatcherLog() {} + +template <typename T, typename... Rest> +void DispatcherLog(const T& first, const Rest&... rest) { + if constexpr (std::is_same_v<LogErrNo, T>) { + 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; + } + DispatcherLog(err_buf); + } else if constexpr (std::is_same_v<LogLnSync, T>) { + 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); + } + } + DispatcherLog(rest...); +} + +inline void DispatcherCheck(bool condition, std::string_view error) { + if (!condition) { + DispatcherLog(error, LogLnSync{}); + std::_Exit(1); + } +} + +const char* GetDispatcherFlags() { + static auto dispatcher_flags = []() -> const char* { + // TODO(xinhaoyuan): Rename the env name to FUZZTEST_DISPATCHER_FLAGS. + const char* env_flags = std::getenv("CENTIPEDE_RUNNER_FLAGS"); + if (env_flags == nullptr) return nullptr; + const char* result = strdup(env_flags); + DispatcherCheck(result != nullptr, "Cannot copy the dispatcher flags"); + return result; + }(); + return dispatcher_flags; +} + +std::optional<std::string_view> GetDispatcherFlag( + const char* absl_nonnull flag_header) { + const char* dispatcher_flags = GetDispatcherFlags(); + if (dispatcher_flags == nullptr) return std::nullopt; + // Extract "value" from ":flag=value:" + const char* beg = std::strstr(dispatcher_flags, flag_header); + if (!beg) return std::nullopt; + const char* value_beg = beg + std::strlen(flag_header); + const char* value_end = std::strstr(value_beg, ":"); + if (!value_end) return std::nullopt; + return std::string_view{value_beg, + static_cast<size_t>(value_end - value_beg)}; +} + +bool HasDispatcherSwitchFlag(const char* absl_nonnull switch_flag) { + const char* dispatcher_flags = GetDispatcherFlags(); + if (dispatcher_flags == nullptr) return false; + return std::strstr(dispatcher_flags, switch_flag) != nullptr; +} + +enum class DispatcherAction { + kGetBinaryId, + kListTests, + kTestGetSeeds, + kTestMutate, + kTestExecute, +}; + +constexpr char kDispatcherBinaryIdOutputFlagHeader[] = ":binary_id_output="; +constexpr char kDispatcherTestNameFlagHeader[] = ":test="; +constexpr char kDispatcherTestListingPrefixFlagHeader[] = + ":test_listing_prefix="; +constexpr char kDispatcherTestGetSeedsOutputDirFlagHeader[] = + ":arg1="; // TODO: Use better flag names when standardizing the protocol. +constexpr char kDispatcherFailureDescriptionPathFlagHeader[] = + ":failure_description_path="; +constexpr char kDispatcherFailureSignaturePathFlagHeader[] = + ":failure_signature_path="; +constexpr char kDispatcherInputsBlobSequencePathFlagHeader[] = + ":arg1="; // TODO: Use better flag names when standardizing the protocol. +constexpr char kDispatcherOutputsBlobSequencePathFlagHeader[] = + ":arg2="; // TODO: Use better flag names when standardizing the protocol. + +BlobSequence* GetInputsBlobSequence() { + static auto result = []() -> BlobSequence* { + if (std::strstr(GetDispatcherFlags(), ":shmem:") == nullptr) { + return nullptr; + } + auto input_path = + GetDispatcherFlag(kDispatcherInputsBlobSequencePathFlagHeader); + DispatcherCheck(input_path.has_value(), "inputs blob sequence is missing"); + return new SharedMemoryBlobSequence(std::string(*input_path).c_str()); + }(); + return result; +} + +BlobSequence* GetOutputsBlobSequence() { + static auto result = []() -> BlobSequence* { + if (std::strstr(GetDispatcherFlags(), ":shmem:") == nullptr) { + return nullptr; + } + auto output_path = + GetDispatcherFlag(kDispatcherOutputsBlobSequencePathFlagHeader); + DispatcherCheck(output_path.has_value(), + "outputs blob sequence is missing"); + return new SharedMemoryBlobSequence(std::string(*output_path).c_str()); + }(); + return result; +} + +DispatcherAction GetDispatcherAction() { + static DispatcherAction dispatcher_action = [] { + if (HasDispatcherSwitchFlag(":dump_binary_id:")) { + return DispatcherAction::kGetBinaryId; + } + if (HasDispatcherSwitchFlag(":list_tests:")) { + return DispatcherAction::kListTests; + } + if (HasDispatcherSwitchFlag(":dump_seed_inputs:")) { + return DispatcherAction::kTestGetSeeds; + } + auto* inputs_blobseq = GetInputsBlobSequence(); + DispatcherCheck(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 DispatcherAction::kTestMutate; + } + if (IsExecutionRequest(request_type_blob)) { + inputs_blobseq->Reset(); + return DispatcherAction::kTestExecute; + } + DispatcherCheck(false, "unknown dispatcher action from the flags"); + // should not reach here. + std::abort(); + }(); + return dispatcher_action; +} + +template <typename... C> +void TrySetFileContents(const char* absl_nonnull path, C... contents) { + // Needs to be signal-safe. + int f = open(path, O_CREAT | O_TRUNC | O_WRONLY, /*mode=*/0660); + if (f == -1) { + DispatcherLog("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) { + DispatcherLog("write() failed on ", path, ": ", LogErrNo{}, + LogLnSync{}); + return false; + } + if (r == 0) { + DispatcherLog("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) { + DispatcherLog("fsync() failed on ", path, ": ", LogErrNo{}, LogLnSync{}); + } + if (close(f) != 0) { + DispatcherLog("close() failed on ", path, ": ", LogErrNo{}, LogLnSync{}); + } +} + +static std::atomic<bool> in_test_callback = false; + +class TestCallbackGuard { + public: + TestCallbackGuard() { + DispatcherCheck(!in_test_callback.exchange(true), + "test callback is already activated"); + } + + ~TestCallbackGuard() { in_test_callback = false; } +}; + +void DispatcherDoGetBinaryId(const FuzzTestDispatcherCallbacks& callbacks) { + const auto binary_id_output_path = + GetDispatcherFlag(kDispatcherBinaryIdOutputFlagHeader); + DispatcherCheck(binary_id_output_path.has_value(), + "binary ID output path is not set"); + std::string binary_id; + { + TestCallbackGuard guard; + binary_id = callbacks.get_binary_id ? callbacks.get_binary_id() : ""; + } + TrySetFileContents(std::string{*binary_id_output_path}.c_str(), binary_id); +} + +void DispatcherDoListTests(const FuzzTestDispatcherCallbacks& callbacks) { + DispatcherCheck(callbacks.list_tests != nullptr, + "list_tests callback must be set"); + TestCallbackGuard guard; + callbacks.list_tests(); +} + +void DispatcherDoGetSeeds(const FuzzTestDispatcherCallbacks& callbacks) { + if (callbacks.get_seeds == nullptr) { + return; + } + TestCallbackGuard guard; + callbacks.get_seeds(); +} + +int DispatcherDoMutate(const FuzzTestDispatcherCallbacks& callbacks) { + auto* inputs_blobseq = GetInputsBlobSequence(); + auto* outputs_blobseq = GetOutputsBlobSequence(); + DispatcherCheck(inputs_blobseq != nullptr && outputs_blobseq != nullptr, + "inputs/outputs blob sequences must be specified"); + + bool has_mutate = callbacks.mutate != nullptr; + if (!MutationResult::WriteHasCustomMutator(has_mutate, *outputs_blobseq)) { + std::fprintf(stderr, "Failed to write custom mutator indicator!\n"); + return EXIT_FAILURE; + } + if (!has_mutate) { + return EXIT_SUCCESS; + } + + // Read max_num_mutants. + size_t num_mutants = 0; + size_t num_inputs = 0; + if (!IsMutationRequest(inputs_blobseq->Read())) { + std::fprintf(stderr, "Not mutation request!\n"); + return EXIT_FAILURE; + } + if (!IsNumMutants(inputs_blobseq->Read(), num_mutants)) { + std::fprintf(stderr, "No num mutants\n"); + return EXIT_FAILURE; + } + if (!IsNumInputs(inputs_blobseq->Read(), num_inputs)) { + std::fprintf(stderr, "No num inputs\n"); + return EXIT_FAILURE; + } + + struct OwningMutateInput { + ByteArray data; + ExecutionMetadata metadata; + }; + // Note: unclear if we can continue using std::vector (or other STL) + // in the runner. But for now use std::vector. + // + // Collect the inputs into a vector. We copy them instead of using pointers + // into shared memory so that the user code doesn't touch the shared memory. + std::vector<OwningMutateInput> owning_inputs; + owning_inputs.reserve(num_inputs); + std::vector<FuzzTestDispatcherInputForMutate> inputs; + 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; + owning_inputs.push_back( + OwningMutateInput{/*data=*/ByteArray{blob.data, blob.data + blob.size}, + /*metadata=*/std::move(metadata)}); + inputs.push_back(FuzzTestDispatcherInputForMutate{ + /*input=*/owning_inputs.back().data.data(), + /*input_size=*/owning_inputs.back().data.size(), + /*metadata=*/owning_inputs.back().metadata.cmp_data.data(), + /*metadata_size=*/owning_inputs.back().metadata.cmp_data.size()}); + } + + { + TestCallbackGuard guard; + fprintf(stderr, "calling custom mutator\n"); + callbacks.mutate(inputs.data(), inputs.size(), num_mutants, + /*shrink=*/0); + } + return EXIT_SUCCESS; +} + +int DispatcherDoExecute(const FuzzTestDispatcherCallbacks& callbacks) { + DispatcherCheck(callbacks.execute != nullptr, "execute callback must be set"); + auto* inputs_blobseq = GetInputsBlobSequence(); + auto* outputs_blobseq = GetOutputsBlobSequence(); + DispatcherCheck(inputs_blobseq != nullptr && outputs_blobseq != nullptr, + "inputs/ouptuts blob sequence must exist"); + + size_t num_inputs = 0; + DispatcherCheck(IsExecutionRequest(inputs_blobseq->Read()), + "not an execution request"); + DispatcherCheck(IsNumInputs(inputs_blobseq->Read(), num_inputs), + "failed to read num_inputs"); + + for (size_t i = 0; i < num_inputs; i++) { + auto blob = inputs_blobseq->Read(); + if (!blob.IsValid()) return EXIT_SUCCESS; // no more blobs to read. + if (!IsDataInput(blob)) return EXIT_FAILURE; + + // Copy from blob to data so that to not pass the shared memory further. + ByteArray data(blob.data, blob.data + blob.size); + + if (!BatchResult::WriteInputBegin(*outputs_blobseq)) { + // TODO: This is to follow the previous behavior, but should we abort + // here? + break; + } + { + TestCallbackGuard guard; + callbacks.execute(data.data(), data.size()); + } + if (!BatchResult::WriteInputEnd(*outputs_blobseq)) { + // TODO: This is to follow the previous behavior, but should we abort + // here? + break; + } + } + + return EXIT_SUCCESS; +} + +void DispatcherEmitFailure(const char* absl_nonnull prefix, + const char* absl_nonnull description, + const char* signature, size_t signature_size) { + bool success = false; + [[maybe_unused]] static bool write_once = [=, &success] { + if (const auto failure_description_path = + GetDispatcherFlag(kDispatcherFailureDescriptionPathFlagHeader); + failure_description_path.has_value()) { + TrySetFileContents(std::string{*failure_description_path}.c_str(), prefix, + description); + } + if (const auto failure_signature_path = + GetDispatcherFlag(kDispatcherFailureSignaturePathFlagHeader); + failure_signature_path.has_value()) { + TrySetFileContents(std::string{*failure_signature_path}.c_str(), + std::string_view{signature, signature_size}); + } + success = true; + return true; + }(); + if (!success) { + DispatcherLog("Failed to emit failure ", prefix, description, LogLnSync{}); + } +} + +} // namespace + +} // namespace fuzztest::internal + +using fuzztest::internal::BatchResult; +using fuzztest::internal::DispatcherAction; +using fuzztest::internal::DispatcherCheck; +using fuzztest::internal::DispatcherDoExecute; +using fuzztest::internal::DispatcherDoGetBinaryId; +using fuzztest::internal::DispatcherDoGetSeeds; +using fuzztest::internal::DispatcherDoListTests; +using fuzztest::internal::DispatcherDoMutate; +using fuzztest::internal::DispatcherEmitFailure; +using fuzztest::internal::GetDispatcherAction; +using fuzztest::internal::GetDispatcherFlag; +using fuzztest::internal::GetDispatcherFlags; +using fuzztest::internal::GetOutputsBlobSequence; +using fuzztest::internal::HasDispatcherSwitchFlag; +using fuzztest::internal::in_test_callback; +using fuzztest::internal::kDispatcherTestGetSeedsOutputDirFlagHeader; +using fuzztest::internal::kDispatcherTestListingPrefixFlagHeader; +using fuzztest::internal::kDispatcherTestNameFlagHeader; +using fuzztest::internal::MutationResult; + +int FuzzTestDispatcherIsEnabled() { + const char* flags = GetDispatcherFlags(); + if (flags == nullptr) return 0; + fprintf(stderr, "Dispatcher is enabled with flags: %s\n", flags); + return 1; +} + +const char* FuzzTestDispatcherGetTestName() { + static auto test_name = []() -> const char* { + const auto test_name = GetDispatcherFlag(kDispatcherTestNameFlagHeader); + if (!test_name.has_value()) return nullptr; + return strndup(test_name->data(), test_name->size()); + }(); + return test_name; +} + +int FuzzTestDispatcherRun(const FuzzTestDispatcherCallbacks* callbacks) { + DispatcherCheck(callbacks != nullptr, "callbacks must be set"); + if (HasDispatcherSwitchFlag(":dump_configuration:")) { + return 0; + } + switch (GetDispatcherAction()) { + case DispatcherAction::kGetBinaryId: + DispatcherDoGetBinaryId(*callbacks); + break; + case DispatcherAction::kListTests: + DispatcherDoListTests(*callbacks); + break; + case DispatcherAction::kTestGetSeeds: + DispatcherDoGetSeeds(*callbacks); + break; + case DispatcherAction::kTestMutate: + DispatcherDoMutate(*callbacks); + break; + case DispatcherAction::kTestExecute: + DispatcherDoExecute(*callbacks); + break; + default: + DispatcherCheck(false, "unknown dispatcher action to take"); + } + return 0; +} + +void FuzzTestDispatcherEmitTestName(const char* name) { + DispatcherCheck( + GetDispatcherAction() == DispatcherAction::kListTests && in_test_callback, + "must be called inside test callback for listing tests"); + static auto test_listing_prefix = + GetDispatcherFlag(kDispatcherTestListingPrefixFlagHeader); + DispatcherCheck(test_listing_prefix.has_value(), + "test listing path prefix must be set"); + DispatcherCheck(name != nullptr, "test name must be set"); + auto test_output_path = std::string{*test_listing_prefix}; + test_output_path += name; + FILE* f = std::fopen(test_output_path.c_str(), "w"); + if (f == nullptr) { + std::perror("FAILURE: fopen()"); + } + std::fclose(f); +} + +void FuzzTestDispatcherEmitSeed(const void* data, size_t size) { + DispatcherCheck(GetDispatcherAction() == DispatcherAction::kTestGetSeeds && + in_test_callback, + "must be called inside test callback for getting seeds"); + DispatcherCheck(size > 0 && data != nullptr, + "seed must be non-empty with a valid pointer"); + static size_t seed_index = 0; + static const char* output_dir = [] { + const auto flag_value = + GetDispatcherFlag(kDispatcherTestGetSeedsOutputDirFlagHeader); + DispatcherCheck(flag_value.has_value(), + "seeds output path must be specified"); + const char* result = strndup(flag_value->data(), flag_value->size()); + DispatcherCheck(result != nullptr, "failed to copy the seeds output path"); + return result; + }(); + // Cap seed index within 9 digits. If this was triggered, the dumping would + // take forever.. + if (seed_index >= 1000000000) return; + char seed_path_buf[PATH_MAX]; + const size_t num_path_chars = + snprintf(seed_path_buf, PATH_MAX, "%s/%09lu", output_dir, seed_index); + DispatcherCheck(num_path_chars < PATH_MAX, "seed path reaches PATH_MAX"); + FILE* output_file = fopen(seed_path_buf, "w"); + const size_t num_bytes_written = fwrite(data, 1, size, output_file); + DispatcherCheck(num_bytes_written == size, + "wrong number of bytes written for seed"); + fclose(output_file); + ++seed_index; +} + +void FuzzTestDispatcherEmitMutant(const void* data, size_t size) { + DispatcherCheck(GetDispatcherAction() == DispatcherAction::kTestMutate && + in_test_callback, + "must be called inside test callback for mutating"); + DispatcherCheck(size > 0 && data != nullptr, + "mutant must be non-empty with a valid pointer"); + auto* output = GetOutputsBlobSequence(); + DispatcherCheck(output != nullptr, "outputs blob sequence must exist"); + DispatcherCheck(MutationResult::WriteMutant( + {static_cast<const uint8_t*>(data), size}, *output), + "failed to write mutant"); +} + +void FuzzTestDispatcherEmitFeedbackAs32BitFeatures(const uint32_t* features, + size_t num_features) { + DispatcherCheck(GetDispatcherAction() == DispatcherAction::kTestExecute && + in_test_callback, + "must be called inside test callback of executing"); + DispatcherCheck(num_features > 0 && features != nullptr, + "feature array must be non-empty with a valid pointer"); + auto* output = GetOutputsBlobSequence(); + DispatcherCheck(output != nullptr, "outputs blob sequence must exist"); + DispatcherCheck(BatchResult::WriteDispatcher32BitFeatures( + features, num_features, *output), + "failed to write feedback"); +} + +void FuzzTestDispatcherEmitExecutionMetadata(const void* metadata, + size_t size) { + DispatcherCheck(GetDispatcherAction() == DispatcherAction::kTestExecute && + in_test_callback, + "must be called inside test callback of executing"); + DispatcherCheck(size > 0 && metadata != nullptr, + "metadata must be non-empty with a valid pointer"); + auto* output = GetOutputsBlobSequence(); + DispatcherCheck(output != nullptr, "outputs blob sequence must exist"); + DispatcherCheck(BatchResult::WriteMetadata( + {static_cast<const uint8_t*>(metadata), size}, *output), + "failed to write metadata"); +} + +void FuzzTestDispatcherEmitInputFailure(const char* description, + const void* signature, + size_t signature_size) { + DispatcherCheck(GetDispatcherAction() == DispatcherAction::kTestExecute && + in_test_callback, + "must be called inside test callback for executing"); + DispatcherCheck((signature == nullptr) == (signature_size == 0), + "violated invariant: signature should be nullptr if and only " + "if signature_size is 0"); + DispatcherEmitFailure( + "INPUT FAILURE: ", description != nullptr ? description : "", + reinterpret_cast<const char*>(signature), signature_size); +} + +void FuzzTestDispatcherEmitIgnoredFailure(const char* description) { + DispatcherEmitFailure( + "IGNORED FAILURE: ", description != nullptr ? description : "", + /*signature=*/nullptr, /*signature_size=*/0); +} + +void FuzzTestDispatcherEmitSetupFailure(const char* description) { + DispatcherEmitFailure( + "SETUP FAILURE: ", description != nullptr ? description : "", + /*signature=*/nullptr, /*signature_size=*/0); +} + +void FuzzTestDispatcherEmitSkippedTestFailure(const char* description) { + DispatcherEmitFailure( + "SKIPPED TEST: ", description != nullptr ? description : "", + /*signature=*/nullptr, /*signature_size=*/0); +}
diff --git a/centipede/dispatcher.h b/centipede/dispatcher.h new file mode 100644 index 0000000..63f4f08 --- /dev/null +++ b/centipede/dispatcher.h
@@ -0,0 +1,141 @@ +// Copyright 2025 The Centipede 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 THIRD_PARTY_CENTIPEDE_DISPATCHER_H_ +#define THIRD_PARTY_CENTIPEDE_DISPATCHER_H_ + +// Dispatcher interface. +// +// This header needs to be C compatible. + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +// Inputs to perform mutations. +struct FuzzTestDispatcherInputForMutate { + const void* input; + size_t input_size; + const void* metadata; + size_t metadata_size; +}; + +// Callbacks to be provided by the fuzz testing framework to +// `FuzzTestDispatcherRun`. +struct FuzzTestDispatcherCallbacks { + // Optional callback to return an ID for the current binary. If not + // implemented, the controller will generate a default ID based on the binary + // path. + const char* (*get_binary_id)(); + // Callback to emit the list of available tests in the binary using + // `FuzzTestDispatcherEmitTestName`. + void (*list_tests)(); + // Callback to emit the seed inputs for a test using + // `FuzzTestDispatcherEmitSeed`. + void (*get_seeds)(); + // Optional callback to emit at most `num_mutants` from `inputs` with + // `num_inputs` entries using `FuzzTestDispatcherEmitMutant`. `shrink` != 0 + // means to generate smaller mutants than the inputs used for mutation. If not + // implemented, the controller will perform basic string-based mutations. + // + // TODO: xinhaoyuan - Reconsider mutation interface design instead of + // following the existing Centipede/runner protocol. + void (*mutate)(const struct FuzzTestDispatcherInputForMutate* inputs, + size_t num_inputs, size_t num_mutants, int shrink); + // Callback to execute `input` with `size` bytes. The callback should emit + // coverage feedback using `FuzzTestDispatcherEmitFeedback*` functions, and + // any metadata for further mutation using + // `FuzzTestDispatEmitExecutionMetadata`. In case the input caused a failure, + // the callback should emit the failure using + // `FuzzTestDispatcherEmitInputFailure`. + void (*execute)(const void* input, size_t size); +}; + +// Functions provided by the FuzzTest engine. + +// Returns 0 if the dispatcher mode is not enabled in the current process; 1 if +// the dispatcher mode is enabled; other values for unexpected errors. +int FuzzTestDispatcherIsEnabled(); + +// All functions below should be called only after `FuzzTestDispatcherIsEnabled` +// returns 1 in the current process. + +// Returns the test name under operation as an unowned, static, and +// null-terminated string. Returns nullptr if the current process is not +// operating on a specific test. +const char* FuzzTestDispatcherGetTestName(); + +// Give control to the FuzzTest engine to invoke `callbacks`. Returns an exit +// code for the current process desired by the engine. +int FuzzTestDispatcherRun(const struct FuzzTestDispatcherCallbacks* callbacks); + +// Emits a test name. Must be called from the `list_tests` callback. `name` must +// be a null-terminated string. +void FuzzTestDispatcherEmitTestName(const char* name); + +// Emits a seed input. Must be called from the `get_seeds` callback. `data` must +// not be nullptr and `size > 0` must hold. +void FuzzTestDispatcherEmitSeed(const void* data, size_t size); + +// Emits a mutant. Must be called from the `mutate` callback. `data` must not be +// nullptr and `size > 0` must hold. +void FuzzTestDispatcherEmitMutant(const void* data, size_t size); + +// Emits coverage feedback for the current input as an array of 32-bit features. +// +// For each 32-bit feature, the bit [31] is ignored; the 4 bits [30-27] +// indicate the feature domain for engine prioritization. The remaining 27 bits +// [26-0] represent the actual 27-bit feature ID in the domain. +// +// Must be called from the `execute` callback. `features` must not be nullptr +// and `num_features > 0` must hold. +void FuzzTestDispatcherEmitFeedbackAs32BitFeatures(const uint32_t* features, + size_t num_features); +// Emits metadata of the current input as raw bytes. Must be called from +// the `execute` callback. +void FuzzTestDispatcherEmitExecutionMetadata(const void* metadata, size_t size); + +// Functions for emitting various types of failures. After calling any of these +// functions, later calls of these functions would have no effect, and the +// current process should exit after necessary cleanup. + +// Emits a failure caused by executing an input. Must be called within the +// `execute` callback. `description` should be a null-terminated string, or +// nullptr can be passed for an empty string; `signature` should be nullptr if +// and only if `signature_size == 0`. +void FuzzTestDispatcherEmitInputFailure(const char* description, + const void* signature, + size_t signature_size); + +// Emits a failure that should be ignored (i.e. not affecting the fuzzing +// workflows). `description` should be a null-terminated string, or nullptr can +// be passed for an empty string. +void FuzzTestDispatcherEmitIgnoredFailure(const char* description); + +// Emits a failure caused by the test setup. `description` should be a +// null-terminated string, or nullptr can be passed for an empty string. +void FuzzTestDispatcherEmitSetupFailure(const char* description); + +// Emits a failure due to reasons to skip the entire test. `description` should +// be a null-terminated string, or nullptr can be passed for an empty string. +void FuzzTestDispatcherEmitSkippedTestFailure(const char* description); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif