diff --git a/centipede/testing/BUILD b/centipede/testing/BUILD index e40ae58de..411849992 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 = [ + ":test_binary_for_engine_testing", + "@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 = "test_binary_for_engine_testing", + srcs = ["test_binary_for_engine_testing.cc"], + deps = [ + "@abseil-cpp//absl/strings", + "@com_google_fuzztest//centipede:engine_abi", + "@com_google_fuzztest//centipede:engine_controller_with_subprocess", + "@com_google_fuzztest//centipede:engine_worker", + ], +) diff --git a/centipede/testing/engine_test.sh b/centipede/testing/engine_test.sh new file mode 100755 index 000000000..9490b3a9e --- /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 test_binary_for_engine_testing 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 \ + TEST_BINARY_FOR_ENGINE_TESTING "${CENTIPEDE_TEST_SRCDIR}/testing/test_binary_for_engine_testing" + +# --- Test 1: Run centipede directly with test_binary_for_engine_testing as target --- +echo "============ Running Test 1: Centipede -> test_binary_for_engine_testing" + +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="${TEST_BINARY_FOR_ENGINE_TESTING}" \ + --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 test_binary_for_engine_testing directly with FUZZTEST_CENTIPEDE_BINARY_PATH --- +echo "============ Running Test 2: test_binary_for_engine_testing (controller) -> Centipede -> test_binary_for_engine_testing (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 TEST_BINARY_FOR_ENGINE_TESTING. +( + cd "${WD2}" + set +e + FUZZTEST_CENTIPEDE_BINARY_PATH="${CENTIPEDE_BINARY}" "${TEST_BINARY_FOR_ENGINE_TESTING}" > "${LOG2}" 2>&1 + RC2=$? + set -e + + cat "${LOG2}" + + if [ $RC2 -eq 0 ]; then + echo "Test 2 failed: TEST_BINARY_FOR_ENGINE_TESTING 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/test_binary_for_engine_testing.cc b/centipede/testing/test_binary_for_engine_testing.cc new file mode 100644 index 000000000..780c2c149 --- /dev/null +++ b/centipede/testing/test_binary_for_engine_testing.cc @@ -0,0 +1,227 @@ +// Copyright 2026 The FuzzTest Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "./centipede/engine_abi.h" +#include "./centipede/engine_controller_abi.h" +#include "./centipede/engine_worker_abi.h" + +namespace { + +std::string_view GetWorkerTestParam() { + static auto worker_test_param = []() -> std::string_view { + const char* env = std::getenv("FUZZTEST_WORKER_TEST_PARAM"); + if (!env) return ""; + return strdup(env); + }(); + return worker_test_param; +} + +FuzzTestBytesView ToBytesView(std::string_view sv) { + return {reinterpret_cast(sv.data()), sv.size()}; +} + +void SetUpCoverageDomains(FuzzTestAdapterCtx* ctx, + const FuzzTestCoverageDomainRegistry* registry) { + static constexpr std::string_view kDomainName = "base"; + const FuzzTestCoverageDomain domain = { + /*domain_id=*/0, + /*name=*/ToBytesView(kDomainName), + /*feature_id_bit_size=*/27, + /*counter_bit_size=*/0, + }; + registry->Register(registry->ctx, &domain); +} + +struct AdapterCtx { + const FuzzTestDiagnosticSink* diagnostic_sink; +}; + +struct TestInput { + std::string content; +}; + +void EmitInput(std::string_view input_content, const FuzzTestInputSink* sink) { + sink->Emit(sink->ctx, reinterpret_cast( + new TestInput{std::string{input_content}})); +} + +void GetRandomSeedInput(FuzzTestAdapterCtx* ctx, + const FuzzTestInputSink* sink) { + EmitInput("random_seed", sink); +} + +void Mutate(FuzzTestAdapterCtx* ctx, FuzzTestInputHandle input, int shrink, + const FuzzTestInputSink* sink) { + const auto* input_object = reinterpret_cast(input); + if (input_object->content == "random_seed") { + EmitInput("mutant_1", sink); + } else if (input_object->content == "mutant_1") { + EmitInput("mutant_2", sink); + } else if (input_object->content == "mutant_2") { + EmitInput("mutant_3", sink); + } else { + EmitInput("bad_input", sink); + } +} + +void AddFeature(uint32_t domain, uint32_t feature, uint32_t counter, + std::vector& out) { + out.push_back((static_cast(domain) << 59) | + (static_cast(feature) << 32) | counter); +} + +void Execute(FuzzTestAdapterCtx* ctx, FuzzTestInputHandle input, + const FuzzTestFeedbackSink* sink) { + std::vector features; + auto* adapter_ctx = reinterpret_cast(ctx); + const auto* input_object = reinterpret_cast(input); + if (input_object->content == "random_seed") { + AddFeature(0, 0, 0, features); + AddFeature(0, 1, 0, features); + AddFeature(0, 2, 0, features); + } else if (input_object->content == "mutant_1") { + AddFeature(0, 3, 0, features); + AddFeature(0, 4, 0, features); + } else if (input_object->content == "mutant_2") { + AddFeature(0, 5, 0, features); + } else if (input_object->content == "mutant_3") { + static constexpr std::string_view kDescription = "some_failure_description"; + static constexpr std::string_view kSignature = "some_signature"; + const auto description_view = ToBytesView(kDescription); + const auto signature_view = ToBytesView(kSignature); + adapter_ctx->diagnostic_sink->EmitFinding( + adapter_ctx->diagnostic_sink->ctx, &description_view, &signature_view); + } else { + static constexpr std::string_view kDescription = + "some_other_failure_description"; + static constexpr std::string_view kSignature = "some_other_signature"; + const auto description_view = ToBytesView(kDescription); + const auto signature_view = ToBytesView(kSignature); + adapter_ctx->diagnostic_sink->EmitFinding( + adapter_ctx->diagnostic_sink->ctx, &description_view, &signature_view); + } + const auto features_view = FuzzTestUint64sView{ + features.data(), + features.size(), + }; + sink->EmitCoverageFeatures(sink->ctx, &features_view); +} + +void DeserializeInputContent(FuzzTestAdapterCtx* ctx, + const FuzzTestBytesView* content, + const FuzzTestInputSink* sink) { + auto* input = new TestInput{ + std::string{reinterpret_cast(content->data), content->size}}; + sink->Emit(sink->ctx, reinterpret_cast(input)); +} + +void SerializeInputContent(FuzzTestAdapterCtx* ctx, FuzzTestInputHandle input, + const FuzzTestBytesSink* sink) { + auto* input_object = reinterpret_cast(input); + const FuzzTestBytesView bytes = { + /*data=*/reinterpret_cast(input_object->content.data()), + /*size=*/input_object->content.size(), + }; + sink->Emit(sink->ctx, &bytes); +} + +void FreeInput(FuzzTestAdapterCtx* ctx, FuzzTestInputHandle input) { + delete reinterpret_cast(input); +} + +void FreeCtx(FuzzTestAdapterCtx* ctx) { + delete reinterpret_cast(ctx); +} + +void ConstructAdapter(const FuzzTestDiagnosticSink* sink, + FuzzTestAdapter* adapter_out) { + adapter_out->ctx = + reinterpret_cast(new AdapterCtx{sink}); + adapter_out->SetUpCoverageDomains = SetUpCoverageDomains; + adapter_out->GetRandomSeedInput = GetRandomSeedInput; + adapter_out->Mutate = Mutate; + adapter_out->Execute = Execute; + adapter_out->DeserializeInputContent = DeserializeInputContent; + adapter_out->SerializeInputContent = SerializeInputContent; + adapter_out->FreeInput = FreeInput; + adapter_out->FreeCtx = FreeCtx; +} + +FuzzTestControllerStatus ControllerRun(const FuzzTestAdapterManager* manager, + const std::vector& flags) { + std::vector flags_bytes_view_list; + flags_bytes_view_list.reserve(flags.size()); + for (const auto& flag : flags) { + flags_bytes_view_list.push_back(FuzzTestBytesView{ + /*data=*/reinterpret_cast(flag.data()), + /*size=*/flag.size(), + }); + } + const FuzzTestBytesViews flags_bytes_views = { + /*views=*/flags_bytes_view_list.data(), + /*count=*/flags_bytes_view_list.size(), + }; + return FuzzTestControllerRun(manager, &flags_bytes_views); +} + +} // namespace + +int main(int argc, char** argv) { + FuzzTestAdapterManager manager = { + /*ctx=*/nullptr, + /*GetBinaryId=*/ + [](FuzzTestAdapterManagerCtx* ctx, const FuzzTestBytesSink* sink) { + static constexpr std::string_view kBinaryId = "some_binary_id"; + const auto bytes = ToBytesView(kBinaryId); + sink->Emit(sink->ctx, &bytes); + }, + /*GetTestName=*/ + [](FuzzTestAdapterManagerCtx* ctx, const FuzzTestBytesSink* sink) { + static constexpr std::string_view kTestName = "some_test"; + const auto bytes = ToBytesView(kTestName); + sink->Emit(sink->ctx, &bytes); + }, + /*ConstructAdapter=*/ + [](FuzzTestAdapterManagerCtx* ctx, + const FuzzTestDiagnosticSink* diagnostic_sink, + FuzzTestAdapter* adapter_out) { + if (GetWorkerTestParam() == "error_on_construct_adapter") { + static constexpr std::string_view kError = "some error"; + const auto error_bytes = ToBytesView(kError); + 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; +}