blob: d82cd28345d539f93449cafabda3e3d809c88103 [file] [log] [blame] [edit]
// 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");
// We ensure that:
// * `inputs` is a valid pointer to an array of
// `FuzzTestDispatcherInputForMutate` objects with length `num_inputs`.
// * Each object of the array contains a valid `input` pointer to
// `input_size` bytes, and a valid `metadata` pointer to `metadata_size`
// bytes.
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;
// We ensure that `input` is a valid pointer to an array of `size` bytes.
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 FuzzTestDispatcherEmitFeedbackAsRawFeatures(const uint64_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::WriteOneFeatureVec(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);
}