Clear features when the input is rejected PiperOrigin-RevId: 791212313
diff --git a/centipede/runner.cc b/centipede/runner.cc index d998350..416d8cb 100644 --- a/centipede/runner.cc +++ b/centipede/runner.cc
@@ -386,8 +386,7 @@ // If `full_clear==true` clear all coverage anyway - useful to remove the // coverage accumulated during startup. __attribute__((noinline)) // so that we see it in profile. -static void -PrepareCoverage(bool full_clear) { +static void PrepareCoverage(bool full_clear) { state.CleanUpDetachedTls(); if (state.run_time_flags.path_level != 0) { state.ForEachTls([](ThreadLocalRunnerState &tls) { @@ -445,12 +444,14 @@ // Adds a kPCs and/or k8bitCounters feature to `g_features` based on arguments. // `idx` is a pc_index. // `counter_value` (non-zero) is a counter value associated with that PC. -static void AddPcIndxedAndCounterToFeatures(size_t idx, uint8_t counter_value) { +static void AddPcIndxedAndCounterToFeatures( + size_t idx, uint8_t counter_value, + const std::function<void(feature_t)> &feature_handler) { if (state.run_time_flags.use_pc_features) { - MaybeAddFeature(feature_domains::kPCs.ConvertToMe(idx)); + feature_handler(feature_domains::kPCs.ConvertToMe(idx)); } if (state.run_time_flags.use_counter_features) { - MaybeAddFeature(feature_domains::k8bitCounters.ConvertToMe( + feature_handler(feature_domains::k8bitCounters.ConvertToMe( Convert8bitCounterToNumber(idx, counter_value))); } } @@ -463,66 +464,72 @@ // LibFuzzer supports this return value as of 2022-07: // https://llvm.org/docs/LibFuzzer.html#rejecting-unwanted-inputs __attribute__((noinline)) // so that we see it in profile. -static void -PostProcessCoverage(int target_return_value) { +static void PostProcessCoverage(int target_return_value) { state.g_features.clear(); - if (target_return_value == -1) return; + std::function<void(feature_t)> feature_handler = MaybeAddFeature; + if (target_return_value == -1) { + // When suppressing a test, still iterate through all of the features as a + // side effect of iteration is zeroing them out. But don't write the + // features anywhere. + feature_handler = [](feature_t feature) {}; + } // Convert counters to features. state.pc_counter_set.ForEachNonZeroByte( - [](size_t idx, uint8_t value) { - AddPcIndxedAndCounterToFeatures(idx, value); + [&feature_handler](size_t idx, uint8_t value) { + AddPcIndxedAndCounterToFeatures(idx, value, feature_handler); }, 0, state.actual_pc_counter_set_size_aligned); // Convert data flow bit set to features. if (state.run_time_flags.use_dataflow_features) { - state.data_flow_feature_set.ForEachNonZeroBit([](size_t idx) { - MaybeAddFeature(feature_domains::kDataFlow.ConvertToMe(idx)); - }); + state.data_flow_feature_set.ForEachNonZeroBit( + [&feature_handler](size_t idx) { + feature_handler(feature_domains::kDataFlow.ConvertToMe(idx)); + }); } // Convert cmp bit set to features. if (state.run_time_flags.use_cmp_features) { // TODO(kcc): remove cmp_feature_set. - state.cmp_feature_set.ForEachNonZeroBit([](size_t idx) { - MaybeAddFeature(feature_domains::kCMP.ConvertToMe(idx)); + state.cmp_feature_set.ForEachNonZeroBit([&feature_handler](size_t idx) { + feature_handler(feature_domains::kCMP.ConvertToMe(idx)); }); - state.cmp_eq_set.ForEachNonZeroBit([](size_t idx) { - MaybeAddFeature(feature_domains::kCMPEq.ConvertToMe(idx)); + state.cmp_eq_set.ForEachNonZeroBit([&feature_handler](size_t idx) { + feature_handler(feature_domains::kCMPEq.ConvertToMe(idx)); }); - state.cmp_moddiff_set.ForEachNonZeroBit([](size_t idx) { - MaybeAddFeature(feature_domains::kCMPModDiff.ConvertToMe(idx)); + state.cmp_moddiff_set.ForEachNonZeroBit([&feature_handler](size_t idx) { + feature_handler(feature_domains::kCMPModDiff.ConvertToMe(idx)); }); - state.cmp_hamming_set.ForEachNonZeroBit([](size_t idx) { - MaybeAddFeature(feature_domains::kCMPHamming.ConvertToMe(idx)); + state.cmp_hamming_set.ForEachNonZeroBit([&feature_handler](size_t idx) { + feature_handler(feature_domains::kCMPHamming.ConvertToMe(idx)); }); - state.cmp_difflog_set.ForEachNonZeroBit([](size_t idx) { - MaybeAddFeature(feature_domains::kCMPDiffLog.ConvertToMe(idx)); + state.cmp_difflog_set.ForEachNonZeroBit([&feature_handler](size_t idx) { + feature_handler(feature_domains::kCMPDiffLog.ConvertToMe(idx)); }); } // Convert path bit set to features. if (state.run_time_flags.path_level != 0) { - state.path_feature_set.ForEachNonZeroBit([](size_t idx) { - MaybeAddFeature(feature_domains::kBoundedPath.ConvertToMe(idx)); + state.path_feature_set.ForEachNonZeroBit([&feature_handler](size_t idx) { + feature_handler(feature_domains::kBoundedPath.ConvertToMe(idx)); }); } // Iterate all threads and get features from TLS data. - state.ForEachTls([](ThreadLocalRunnerState &tls) { + state.ForEachTls([&feature_handler](ThreadLocalRunnerState &tls) { if (state.run_time_flags.callstack_level != 0) { RunnerCheck(tls.top_frame_sp >= tls.lowest_sp, "bad values of tls.top_frame_sp and tls.lowest_sp"); size_t sp_diff = tls.top_frame_sp - tls.lowest_sp; - MaybeAddFeature(feature_domains::kCallStack.ConvertToMe(sp_diff)); + feature_handler(feature_domains::kCallStack.ConvertToMe(sp_diff)); } }); if (state.run_time_flags.callstack_level != 0) { - state.callstack_set.ForEachNonZeroBit([](size_t idx) { - MaybeAddFeature(feature_domains::kCallStack.ConvertToMe(idx)); + state.callstack_set.ForEachNonZeroBit([&feature_handler](size_t idx) { + feature_handler(feature_domains::kCallStack.ConvertToMe(idx)); }); } @@ -538,7 +545,7 @@ // available. If a user domain ID is out of range, alias it to an existing // domain. This is kinder than silently dropping the feature. user_domain_id %= std::size(feature_domains::kUserDomains); - MaybeAddFeature(feature_domains::kUserDomains[user_domain_id].ConvertToMe( + feature_handler(feature_domains::kUserDomains[user_domain_id].ConvertToMe( user_feature_id)); *p = 0; // cleanup for the next iteration. } @@ -549,8 +556,8 @@ if (state.run_time_flags.use_pc_features || state.run_time_flags.use_counter_features) { state.sancov_objects.ForEachNonZeroInlineCounter( - [](size_t idx, uint8_t counter_value) { - AddPcIndxedAndCounterToFeatures(idx, counter_value); + [&feature_handler](size_t idx, uint8_t counter_value) { + AddPcIndxedAndCounterToFeatures(idx, counter_value, feature_handler); }); } } @@ -654,9 +661,8 @@ // Runs one input provided in file `input_path`. // Produces coverage data in file `input_path`-features. __attribute__((noinline)) // so that we see it in profile. -static void -ReadOneInputExecuteItAndDumpCoverage(const char *input_path, - RunnerCallbacks &callbacks) { +static void ReadOneInputExecuteItAndDumpCoverage(const char *input_path, + RunnerCallbacks &callbacks) { // Read the input. auto data = ReadBytesFromFilePath<uint8_t>(input_path);
diff --git a/centipede/testing/BUILD b/centipede/testing/BUILD index 67cfe46..b88acf9 100644 --- a/centipede/testing/BUILD +++ b/centipede/testing/BUILD
@@ -103,6 +103,10 @@ name = "test_fuzz_target", ) +centipede_fuzz_target( + name = "random_rejecting_fuzz_target", +) + # Dummy fuzz target that uses the C++ RunnerCallbacks API to return seed inputs. cc_binary( name = "_seeded_fuzz_target", @@ -339,6 +343,7 @@ data = [ ":test_fuzz_target", ":test_fuzz_target_non_pie", + "@com_google_fuzztest//centipede", "@com_google_fuzztest//centipede:test_util_sh", ], ) @@ -412,6 +417,16 @@ ) sh_test( + name = "runner_cleanup_test", + srcs = ["runner_cleanup_test.sh"], + data = [ + ":random_rejecting_fuzz_target", + "@com_google_fuzztest//centipede", + "@com_google_fuzztest//centipede:test_util_sh", + ], +) + +sh_test( name = "user_defined_features_test", srcs = ["user_defined_features_test.sh"], data = [
diff --git a/centipede/testing/random_rejecting_fuzz_target.cc b/centipede/testing/random_rejecting_fuzz_target.cc new file mode 100644 index 0000000..84a8b89 --- /dev/null +++ b/centipede/testing/random_rejecting_fuzz_target.cc
@@ -0,0 +1,36 @@ +// 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 <cstddef> +#include <cstdint> + +static constexpr size_t kNumExtraFeatures = 10000; // Any number. +__attribute__((used, retain, + section("__centipede_extra_features"))) static uint64_t + extra_features[kNumExtraFeatures]; + +static uint64_t run_number = 0; + +// "Randomly" fails half the time, but puts a user feature into the array when +// it does. This allows us to verify that inputs that return -1 are properly +// cleaned up and do not leak user features into future inputs that return 0. +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + ++run_number; + if (run_number % 2 == 0) { + uint64_t domain = 1; + extra_features[0] = (domain << 32) | (1 << 8) | 1; + return -1; + } + return 0; +}
diff --git a/centipede/testing/runner_cleanup_test.sh b/centipede/testing/runner_cleanup_test.sh new file mode 100755 index 0000000..fdde02f --- /dev/null +++ b/centipede/testing/runner_cleanup_test.sh
@@ -0,0 +1,42 @@ +#!/bin/bash + +# 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. + +# Tests cleanup of features for ignored tests. + +set -eu + +source "$(dirname "$0")/../test_util.sh" + +CENTIPEDE_TEST_SRCDIR="$(fuzztest::internal::get_centipede_test_srcdir)" + +echo "======== Check ignored tests don't count features" + +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 \ + REJECTING_BINARY "${CENTIPEDE_TEST_SRCDIR}/testing/random_rejecting_fuzz_target" + +WD="${TEST_TMPDIR}/WD" +LOG="${TEST_TMPDIR}/log" + +"${CENTIPEDE_BINARY}" --binary="${REJECTING_BINARY}" --workdir="${WD}" \ + --num_runs=10000 2>&1 |tee "${LOG}" + +fuzztest::internal::assert_regex_not_in_file "usr1: " "${LOG}" + +echo "PASS"