Add stop request reason string that can be exported. Also added tests for :stop, including a concurrency stress test. PiperOrigin-RevId: 964251184
diff --git a/centipede/BUILD b/centipede/BUILD index 1259192..d486c85 100644 --- a/centipede/BUILD +++ b/centipede/BUILD
@@ -1451,6 +1451,17 @@ ) cc_test( + name = "stop_test", + srcs = ["stop_test.cc"], + deps = [ + ":stop", + ":thread_pool", + "@abseil-cpp//absl/time", + "@googletest//:gtest_main", + ], +) + +cc_test( name = "rusage_stats_test", size = "medium", timeout = "long",
diff --git a/centipede/centipede.cc b/centipede/centipede.cc index e2a0ee3..16df2e0 100644 --- a/centipede/centipede.cc +++ b/centipede/centipede.cc
@@ -467,10 +467,12 @@ success = ExecuteAndReportCrash(extra_binary, inputs, extra_batch_result) && success; } - if (stop_condition_.EarlyStopRequested()) return false; + if (stop_condition_.StopRequested()) return false; if (!success && env_.exit_on_crash) { FUZZTEST_LOG(INFO) << "--exit_on_crash is enabled; exiting soon"; - stop_condition_.RequestEarlyStop(EXIT_FAILURE); + stop_condition_.RequestStop(EXIT_FAILURE, + "A crash was found in the test with " + "--exit_on_crash set for the engine"); return false; } bool batch_gained_new_coverage = false; @@ -1017,7 +1019,7 @@ if (batch_result.IsSkippedTest()) { log_execution_failure("Skipped Test: "); FUZZTEST_LOG(INFO) << "Requesting early stop due to skipped test."; - stop_condition_.RequestEarlyStop(EXIT_SUCCESS); + stop_condition_.RequestStop(EXIT_SUCCESS, "The test was skipped"); return; } @@ -1025,13 +1027,13 @@ log_execution_failure("Test Setup Failure: "); FUZZTEST_LOG(INFO) << "Requesting early stop due to setup failure in the test."; - stop_condition_.RequestEarlyStop(EXIT_FAILURE); + stop_condition_.RequestStop(EXIT_FAILURE, "Setup failed in the test"); return; } - // Skip reporting only if RequestEarlyStop is called - still reporting if time + // Skip reporting only if RequestStop is called - still reporting if time // limit is reached. - if (stop_condition_.EarlyStopRequested()) return; + if (stop_condition_.StopRequested()) return; if (++num_crashes_ > env_.max_num_crash_reports) return;
diff --git a/centipede/centipede_callbacks.cc b/centipede/centipede_callbacks.cc index c615992..1bcb1e3 100644 --- a/centipede/centipede_callbacks.cc +++ b/centipede/centipede_callbacks.cc
@@ -306,9 +306,11 @@ // Check the PC table. if (binary_info.pc_table.empty()) { if (env_.require_pc_table) { - FUZZTEST_LOG(ERROR) << "Could not get PC table; exiting (override with " - "--require_pc_table=false)"; - exit(EXIT_FAILURE); + FUZZTEST_LOG(ERROR) + << "Could not get PC table; requesting to stop (override with " + "--require_pc_table=false)"; + stop_condition_.RequestStop(EXIT_FAILURE, "Could not get PC table"); + return; } FUZZTEST_LOG(WARNING) << "Could not get PC table; CF table and debug symbols will "
diff --git a/centipede/centipede_default_callbacks.cc b/centipede/centipede_default_callbacks.cc index d8b01a3..dc4f055 100644 --- a/centipede/centipede_default_callbacks.cc +++ b/centipede/centipede_default_callbacks.cc
@@ -124,7 +124,8 @@ PrintExecutionLog(); FUZZTEST_LOG(ERROR) << "Test binary failed to mutate inputs at the final " "attempt - exiting."; - stop_condition_.RequestEarlyStop(EXIT_FAILURE); + stop_condition_.RequestStop(EXIT_FAILURE, + "Test binary failed to mutate inputs"); return {}; } }
diff --git a/centipede/centipede_flags.inc b/centipede/centipede_flags.inc index 0709562..c4a246c 100644 --- a/centipede/centipede_flags.inc +++ b/centipede/centipede_flags.inc
@@ -162,6 +162,8 @@ "If set, will ignore reporting timeouts as errors.") CENTIPEDE_FLAG(absl::Duration, runner_cleanup_timeout, absl::Seconds(60), "Cleanup timeout for runner commands.") +CENTIPEDE_FLAG(std::string, stop_reason_file, "", + "If set, will write any stop reason to the local file path.") CENTIPEDE_FLAG( absl::Time, stop_at, absl::InfiniteFuture(), "Stop fuzzing in all shards (--total_shards) at approximately this "
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc index 4a7e80c..e0793d1 100644 --- a/centipede/centipede_interface.cc +++ b/centipede/centipede_interface.cc
@@ -88,8 +88,10 @@ auto blob_reader = DefaultBlobFileReaderFactory(); absl::Status open_status = blob_reader->Open(arg); if (!open_status.ok()) { - FUZZTEST_LOG(INFO) << "Failed to open " << arg << ": " << open_status; - stop_condition.RequestEarlyStop(EXIT_FAILURE); + const std::string stop_reason = + absl::StrCat("Failed to open ", arg, ": ", open_status); + FUZZTEST_LOG(WARNING) << stop_reason; + stop_condition.RequestStop(EXIT_FAILURE, stop_reason); return; } ByteSpan blob; @@ -145,6 +147,9 @@ ScopedCentipedeCallbacks scoped_callbacks(callbacks_factory, env, stop_condition); scoped_callbacks.callbacks()->PopulateBinaryInfo(binary_info); + if (stop_condition.ShouldStop()) { + return binary_info; + } } if (env.save_binary_info) { const std::string binary_info_dir = WorkDir{env}.BinaryInfoDirPath(); @@ -460,10 +465,13 @@ std::string pcs_file_path; BinaryInfo binary_info = PopulateBinaryInfoAndSavePCsIfNecessary( env, callbacks_factory, pcs_file_path, stop_condition); + if (stop_condition.StopRequested()) return; FUZZTEST_LOG(INFO) << "Test shard index: " << test_shard_index << " Total test shards: " << total_test_shards; + StopCondition::StopRequest stop_request; + // Step 2: Run the fuzz test. // Unset stop time. stop_time will be set later. @@ -529,7 +537,7 @@ absl::Cleanup clean_up_workdir = [is_workdir_specified, &env, &stop_condition] { - if (!is_workdir_specified && !stop_condition.EarlyStopRequested()) { + if (!is_workdir_specified && !stop_condition.StopRequested()) { FUZZTEST_CHECK_OK(RemotePathDelete(env.workdir, /*recursively=*/true)); } }; @@ -558,11 +566,11 @@ } is_resuming = false; - if (stop_condition.EarlyStopRequested()) { - if (const auto exit_code = stop_condition.ExitCode(); - exit_code != EXIT_SUCCESS) { + if (stop_condition.StopRequested(&stop_request)) { + if (stop_request.exit_code != EXIT_SUCCESS) { FUZZTEST_LOG(ERROR) << "Early stop requested for test " << env.test_name - << " with failure exit code " << exit_code; + << " with failure exit code " + << stop_request.exit_code; } else { FUZZTEST_LOG(INFO) << "Skipping test " << env.test_name << " due to early stop requested without failure."; @@ -590,11 +598,11 @@ (stats_dir / absl::StrCat("fuzzing_stats_", execution_stamp)).c_str())); } - if (stop_condition.EarlyStopRequested()) { - if (const auto exit_code = stop_condition.ExitCode(); - exit_code != EXIT_SUCCESS) { + if (stop_condition.StopRequested(&stop_request)) { + if (stop_request.exit_code != EXIT_SUCCESS) { FUZZTEST_LOG(ERROR) << "Early stop requested for test " << env.test_name - << " with failure exit code " << exit_code; + << " with failure exit code " + << stop_request.exit_code; } else { FUZZTEST_LOG(INFO) << "Skip updating corpus database due to early stop " "requested without failure."; @@ -717,6 +725,16 @@ } stop_condition->SetStopTime(env.stop_at); + auto SaveStopReasonAndGetExitCode = [&]() -> int { + StopCondition::StopRequest stop_request; + const bool stop_requested = stop_condition->StopRequested(&stop_request); + if (!stop_requested) return EXIT_SUCCESS; + if (!env.stop_reason_file.empty()) { + WriteToLocalFile(env.stop_reason_file, stop_request.reason); + } + return stop_request.exit_code; + }; + if (!env.corpus_to_files.empty()) { Centipede::CorpusToFiles(env, env.corpus_to_files); return EXIT_SUCCESS; @@ -732,14 +750,14 @@ if (!env.for_each_blob.empty()) { ForEachBlob(env, *stop_condition); - return stop_condition->ExitCode(); + return SaveStopReasonAndGetExitCode(); } if (!env.minimize_crash_file_path.empty()) { ByteArray crashy_input; ReadFromLocalFile(env.minimize_crash_file_path, crashy_input); MinimizeCrash(crashy_input, env, callbacks_factory, *stop_condition); - return stop_condition->ExitCode(); + return SaveStopReasonAndGetExitCode(); } // Just export the corpus from a local dir and exit. @@ -815,7 +833,7 @@ } if (env.replay_crash) { ReplayCrash(updated_env, callbacks_factory, *stop_condition); - return stop_condition->ExitCode(); + return SaveStopReasonAndGetExitCode(); } if (env.export_crash) { return ExportCrash(updated_env); @@ -829,7 +847,7 @@ absl::Seconds(1)) << "Time limit per fuzz test must be at least 1 second."; UpdateCorpusDatabase(updated_env, callbacks_factory, *stop_condition); - return stop_condition->ExitCode(); + return SaveStopReasonAndGetExitCode(); } } @@ -842,11 +860,14 @@ std::string pcs_file_path; BinaryInfo binary_info = PopulateBinaryInfoAndSavePCsIfNecessary( env, callbacks_factory, pcs_file_path, *stop_condition); + if (stop_condition->StopRequested()) { + return SaveStopReasonAndGetExitCode(); + } if (env.analyze) return Analyze(env); Fuzz(env, binary_info, pcs_file_path, callbacks_factory, *stop_condition); - return stop_condition->ExitCode(); + return SaveStopReasonAndGetExitCode(); // TODO: fniksic - Report the crash summary here if requested. What are the // binary identifier and the fuzz test name here?
diff --git a/centipede/centipede_main.cc b/centipede/centipede_main.cc index e38dcaf..7983059 100644 --- a/centipede/centipede_main.cc +++ b/centipede/centipede_main.cc
@@ -36,7 +36,7 @@ const char msg[] = "\n[!] Ctrl-C pressed: winding down\n"; [[maybe_unused]] auto write_res = write(STDERR_FILENO, msg, sizeof(msg) - 1); - global_stop_condition.RequestEarlyStop(EXIT_FAILURE); + global_stop_condition.RequestStop(EXIT_FAILURE, "Ctrl-C pressed"); }; sigaction(SIGINT, &sigact, nullptr); }
diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc index 58539ff..021805e 100644 --- a/centipede/centipede_test.cc +++ b/centipede/centipede_test.cc
@@ -1294,8 +1294,9 @@ EXPECT_THAT(callbacks.Mutate(GetMutationInputRefsFromDataInputs(inputs), inputs.size()), IsEmpty()); - EXPECT_TRUE(stop_condition.EarlyStopRequested()); - EXPECT_EQ(stop_condition.ExitCode(), EXIT_FAILURE); + StopCondition::StopRequest stop_request; + EXPECT_TRUE(stop_condition.StopRequested(&stop_request)); + EXPECT_EQ(stop_request.exit_code, EXIT_FAILURE); } TEST_F(CentipedeWithTemporaryLocalDir, @@ -1386,6 +1387,27 @@ HasSubstr("Mutate() succeeded"))); } +TEST_F(CentipedeWithTemporaryLocalDir, CentipedeMainWritesStopReason) { + TempCorpusDir tmp_dir{test_info_->name()}; + Environment env; + env.workdir = tmp_dir.path() / "workdir"; + env.binary = + absl::StrCat(GetDataDependencyFilepath( + "centipede/testing/fuzz_target_with_custom_mutator") + .c_str(), + " --simulate_failure"); + env.stop_reason_file = tmp_dir.path() / "stop_reason"; + env.populate_binary_info = false; + fuzztest::internal::DefaultCallbacksFactory< + fuzztest::internal::CentipedeDefaultCallbacks> + callbacks; + const int ret = CentipedeMain(env, callbacks); + EXPECT_EQ(ret, EXIT_FAILURE); + std::string stop_reason_file_content; + ReadFromLocalFile(env.stop_reason_file, stop_reason_file_content); + EXPECT_THAT(stop_reason_file_content, HasSubstr("failed to mutate")); +} + TEST_F(CentipedeWithTemporaryLocalDir, EngineWorksInWorkerMode) { TempCorpusDir tmp_dir{test_info_->name()}; Environment env;
diff --git a/centipede/command.cc b/centipede/command.cc index 972a056..48ca89f 100644 --- a/centipede/command.cc +++ b/centipede/command.cc
@@ -506,7 +506,8 @@ const auto signal = WTERMSIG(exit_code); if (signal == SIGINT) { if (stop_condition != nullptr) { - stop_condition->RequestEarlyStop(EXIT_FAILURE); + stop_condition->RequestStop( + EXIT_FAILURE, "Command killed: signal=SIGINT (likely Ctrl-C)"); } // When the user kills Centipede via ^C, they are unlikely to be // interested in any of the subprocesses' outputs. Also, ^C terminates all
diff --git a/centipede/command.h b/centipede/command.h index b574a84..33c1d14 100644 --- a/centipede/command.h +++ b/centipede/command.h
@@ -91,7 +91,7 @@ // Waits for the command execution and returns the exit status if the // execution finishes within `deadline`. Must be called only when the command - // is executing. If interrupted, may call `stop_condition->RequestEarlyStop()` + // is executing. If interrupted, may call `stop_condition->RequestStop()` // (see stop.h). std::optional<int> Wait(absl::Time deadline, StopCondition* stop_condition = nullptr);
diff --git a/centipede/minimize_crash.cc b/centipede/minimize_crash.cc index 3a75160..0e12382 100644 --- a/centipede/minimize_crash.cc +++ b/centipede/minimize_crash.cc
@@ -148,7 +148,8 @@ ByteArray original_crashy_input(crashy_input.begin(), crashy_input.end()); if (callbacks->Execute(env.binary, {original_crashy_input}, batch_result)) { FUZZTEST_LOG(INFO) << "The original crashy input did not crash; exiting"; - stop_condition.RequestEarlyStop(EXIT_FAILURE); + stop_condition.RequestStop(EXIT_FAILURE, + "The original crashy input did not crash"); return; } @@ -167,9 +168,9 @@ } } // The threads join here. - if (stop_condition.EarlyStopRequested()) return; + if (stop_condition.StopRequested()) return; if (!queue.SmallerCrashesFound()) { - stop_condition.RequestEarlyStop(EXIT_FAILURE); + stop_condition.RequestStop(EXIT_FAILURE, "Smaller crashes not found"); } }
diff --git a/centipede/minimize_crash_test.cc b/centipede/minimize_crash_test.cc index 03c9ad4..80dc5c0 100644 --- a/centipede/minimize_crash_test.cc +++ b/centipede/minimize_crash_test.cc
@@ -89,25 +89,32 @@ const WorkDir wd{env}; MinimizerMockFactory factory; StopCondition stop_condition; + StopCondition::StopRequest stop_request; // Test with a non-crashy input. + stop_request = {}; MinimizeCrash({1, 2, 3}, env, factory, stop_condition); - EXPECT_EQ(stop_condition.ExitCode(), EXIT_FAILURE); + (void)stop_condition.StopRequested(&stop_request); + EXPECT_EQ(stop_request.exit_code, EXIT_FAILURE); ByteArray expected_minimized = {'f', 'u', 'z'}; // Test with a crashy input that can't be minimized further. - stop_condition.ClearEarlyStopRequest(); + stop_condition.ClearStopRequest(); + stop_request = {}; MinimizeCrash(expected_minimized, env, factory, stop_condition); - EXPECT_EQ(stop_condition.ExitCode(), EXIT_FAILURE); + (void)stop_condition.StopRequested(&stop_request); + EXPECT_EQ(stop_request.exit_code, EXIT_FAILURE); // Test the actual minimization. ByteArray original_crasher = {'f', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', 'u', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', 'z'}; - stop_condition.ClearEarlyStopRequest(); + stop_condition.ClearStopRequest(); + stop_request = {}; MinimizeCrash(original_crasher, env, factory, stop_condition); - EXPECT_EQ(stop_condition.ExitCode(), EXIT_SUCCESS); + (void)stop_condition.StopRequested(&stop_request); + EXPECT_EQ(stop_request.exit_code, EXIT_SUCCESS); // Collect the new crashers from the crasher dir. std::vector<ByteArray> crashers; for (auto const &dir_entry : std::filesystem::directory_iterator{
diff --git a/centipede/stop.cc b/centipede/stop.cc index fb7204a..7b1e35a 100644 --- a/centipede/stop.cc +++ b/centipede/stop.cc
@@ -14,37 +14,78 @@ #include "./centipede/stop.h" +#include <algorithm> +#include <array> #include <atomic> +#include <cstdlib> +#include <cstring> +#include <memory> +#include <string> +#include <string_view> +#include <thread> // NOLINT: for std::this_thread #include "absl/time/clock.h" #include "absl/time/time.h" namespace fuzztest::internal { -bool StopCondition::EarlyStopRequested() const { - return early_stop_.load(std::memory_order_acquire).is_requested; -} +StopCondition::StopCondition() + : reason_(std::make_unique<std::array<char, kReasonBufferSize>>()) {} -void StopCondition::ClearEarlyStopRequest() { - early_stop_.store({}, std::memory_order_release); +void StopCondition::ClearStopRequest() { + if (!stop_requested_.load(std::memory_order_acquire)) { + return; + } + // Wait until the request is fully written. + while (!stop_request_ready_.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + + exit_code_ = EXIT_SUCCESS; + reason_len_ = 0; + + stop_request_ready_.store(false, std::memory_order_release); + stop_requested_.store(false, std::memory_order_release); } void StopCondition::SetStopTime(absl::Time stop_time) { stop_time_ = stop_time; } -void StopCondition::RequestEarlyStop(int exit_code) { - early_stop_.store({exit_code, true}, std::memory_order_release); +bool StopCondition::StopRequested(StopRequest* request) const { + if (!stop_requested_.load(std::memory_order_acquire)) { + return false; + } + if (request == nullptr) { + return true; + } + // Wait until the request is fully written. + while (!stop_request_ready_.load(std::memory_order_acquire)) { + std::this_thread::yield(); + } + request->exit_code = exit_code_; + request->reason = std::string(reason_->data(), reason_len_); + return true; +} + +void StopCondition::RequestStop(int exit_code, std::string_view reason) { + // Only write the reason if it hasn't been requested yet, to avoid races + // overwriting it, although races are rare. + if (stop_requested_.exchange(true)) return; + exit_code_ = exit_code; + // Copy up to the capacity to avoid memory allocation and make it safe to call + // in signal handlers. + reason_len_ = std::min(reason_->size(), reason.size()); + if (reason_len_ > 0) { + std::memcpy(reason_->data(), reason.data(), reason_len_); + } + stop_request_ready_.store(true, std::memory_order_release); } absl::Time StopCondition::GetStopTime() const { return stop_time_; } bool StopCondition::ShouldStop() const { - return EarlyStopRequested() || stop_time_ < absl::Now(); -} - -int StopCondition::ExitCode() const { - return early_stop_.load(std::memory_order_acquire).exit_code; + return StopRequested() || stop_time_ < absl::Now(); } } // namespace fuzztest::internal
diff --git a/centipede/stop.h b/centipede/stop.h index 071fbeb..025694a 100644 --- a/centipede/stop.h +++ b/centipede/stop.h
@@ -15,8 +15,13 @@ #ifndef THIRD_PARTY_CENTIPEDE_STOP_H_ #define THIRD_PARTY_CENTIPEDE_STOP_H_ +#include <array> #include <atomic> +#include <cstddef> #include <cstdlib> +#include <memory> +#include <string> +#include <string_view> #include "absl/time/time.h" @@ -25,32 +30,40 @@ // Encapsulates the stop condition state for Centipede. class StopCondition { public: - StopCondition() = default; + StopCondition(); StopCondition(const StopCondition&) = delete; StopCondition& operator=(const StopCondition&) = delete; StopCondition(StopCondition&&) = delete; StopCondition& operator=(StopCondition&&) = delete; - // Clears the request to stop early. + // Clears the request to stop. // // REQUIRES: Must be called before starting concurrent threads that may invoke // the other methods on this object instance. Specifically, calling this - // function concurrently with `EarlyStopRequested()` is not thread-safe. - void ClearEarlyStopRequest(); + // function concurrently with `StopRequested()` is not thread-safe. + void ClearStopRequest(); - // Returns whether `RequestEarlyStop()` was called or not since the most - // recent call to `ClearEarlyStopRequest()` (if any). + struct StopRequest { + int exit_code = EXIT_SUCCESS; + std::string reason; + }; + + // Returns whether `RequestStop()` was called or not since the most + // recent call to `ClearStopRequest()` (if any). If `request` is not + // null, copy the stop request to the referred instance when stop is + // requested. // - // ENSURES: Thread-safe unless with `ClearEarlyStopRequest()`. - bool EarlyStopRequested() const; + // ENSURES: Thread-safe unless with `ClearStopRequest()`. + bool StopRequested(StopRequest* request = nullptr) const; // Requests that Centipede soon stops whatever it is doing (fuzzing, // minimizing reproducer, etc.), with `exit_code` indicating success (zero) or - // failure (non-zero). + // failure (non-zero). The `reason` will be capped to the internal buffer + // size. // - // ENSURES: Thread-safe and safe to call from signal handlers. - void RequestEarlyStop(int exit_code); + // ENSURES: Thread-safe and safe to call in signal handlers. + void RequestStop(int exit_code, std::string_view reason); // Sets the stop time. // @@ -60,34 +73,31 @@ void SetStopTime(absl::Time stop_time); // Returns true iff it is time to stop, either because the stopping time has - // been reached or `RequestEarlyStop()` was called since the most recent call - // to `ClearEarlyStopRequestAndSetStopTime()` (if any). + // been reached or `RequestStop()` was called since the most recent call + // to `ClearStopRequest()` (if any). // // ENSURES: Thread-safe. bool ShouldStop() const; // Returns the stop time set from the recent - // `ClearEarlyStopRequestAndSetStopTime()`, or `absl::InfiniteFuture()` if it + // `SetStopTime()`, or `absl::InfiniteFuture()` if it // was not set. // // ENSURES: Thread-safe. absl::Time GetStopTime() const; - // Returns the value most recently passed to `RequestEarlyStop()` or 0 if - // `RequestEarlyStop()` was not called since the most recent call to - // `ClearEarlyStopRequestAndSetStopTime()` (if any). - // - // ENSURES: Thread-safe. - int ExitCode() const; - private: - struct EarlyStop { - int exit_code = EXIT_SUCCESS; - bool is_requested = false; - }; - static_assert(std::atomic<EarlyStop>::is_always_lock_free); - std::atomic<EarlyStop> early_stop_{EarlyStop{}}; + static constexpr size_t kReasonBufferSize = 500; + absl::Time stop_time_ = absl::InfiniteFuture(); + // Set to true when `RequestStop` is requested. + std::atomic<bool> stop_requested_ = false; + int exit_code_ = EXIT_SUCCESS; + std::unique_ptr<std::array<char, kReasonBufferSize>> reason_; + size_t reason_len_ = 0; + // Set to true when the fields between `stop_requested_` and + // `stop_request_ready_` are fully set. + std::atomic<bool> stop_request_ready_ = false; }; } // namespace fuzztest::internal
diff --git a/centipede/stop_test.cc b/centipede/stop_test.cc new file mode 100644 index 0000000..b2abd6a --- /dev/null +++ b/centipede/stop_test.cc
@@ -0,0 +1,152 @@ +// Copyright 2026 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/stop.h" + +#include <atomic> +#include <cstdlib> +#include <string> + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "./centipede/thread_pool.h" + +namespace fuzztest::internal { +namespace { + +using ::testing::AllOf; +using ::testing::AnyOf; +using ::testing::Field; +using ::testing::StartsWith; + +TEST(StopConditionTest, InitialState) { + StopCondition stop_condition; + EXPECT_FALSE(stop_condition.StopRequested()); + EXPECT_FALSE(stop_condition.ShouldStop()); + EXPECT_EQ(stop_condition.GetStopTime(), absl::InfiniteFuture()); +} + +TEST(StopConditionTest, RequestStopShortReason) { + StopCondition stop_condition; + stop_condition.RequestStop(EXIT_FAILURE, "test reason"); + + EXPECT_TRUE(stop_condition.StopRequested()); + EXPECT_TRUE(stop_condition.ShouldStop()); + + StopCondition::StopRequest request; + EXPECT_TRUE(stop_condition.StopRequested(&request)); + EXPECT_EQ(request.exit_code, EXIT_FAILURE); + EXPECT_EQ(request.reason, "test reason"); +} + +TEST(StopConditionTest, RequestStopLongReasonIsCapped) { + StopCondition stop_condition; + std::string long_reason(1000, 'a'); + stop_condition.RequestStop(EXIT_FAILURE, long_reason); + + StopCondition::StopRequest request; + EXPECT_TRUE(stop_condition.StopRequested(&request)); + EXPECT_EQ(request.exit_code, EXIT_FAILURE); + // 100 is a reasonable size to check. + EXPECT_GE(request.reason.size(), 100); + EXPECT_THAT(long_reason, StartsWith(request.reason)); +} + +TEST(StopConditionTest, RequestStopOnlyFirstCallTakesEffect) { + StopCondition stop_condition; + stop_condition.RequestStop(EXIT_FAILURE, "first reason"); + stop_condition.RequestStop(EXIT_SUCCESS, "second reason"); + + StopCondition::StopRequest request; + EXPECT_TRUE(stop_condition.StopRequested(&request)); + EXPECT_EQ(request.exit_code, EXIT_FAILURE); + EXPECT_EQ(request.reason, "first reason"); +} + +TEST(StopConditionTest, ClearStopRequest) { + StopCondition stop_condition; + stop_condition.RequestStop(EXIT_FAILURE, "some reason"); + EXPECT_TRUE(stop_condition.StopRequested()); + + stop_condition.ClearStopRequest(); + EXPECT_FALSE(stop_condition.StopRequested()); + + StopCondition::StopRequest request; + EXPECT_FALSE(stop_condition.StopRequested(&request)); + + // Can request stop again after clearing + stop_condition.RequestStop(EXIT_SUCCESS, "new reason"); + EXPECT_TRUE(stop_condition.StopRequested(&request)); + EXPECT_EQ(request.exit_code, EXIT_SUCCESS); + EXPECT_EQ(request.reason, "new reason"); +} + +TEST(StopConditionTest, SetStopTime) { + StopCondition stop_condition; + absl::Time past_time = absl::Now() - absl::Seconds(10); + stop_condition.SetStopTime(past_time); + EXPECT_EQ(stop_condition.GetStopTime(), past_time); + EXPECT_TRUE(stop_condition.ShouldStop()); + EXPECT_FALSE(stop_condition.StopRequested()); +} + +TEST(StopConditionTest, ConcurrentStopRequests) { + StopCondition stop_condition; + ThreadPool requesters(2); + std::atomic<bool> stop_testing = false; + requesters.Schedule([&] { + while (!stop_testing) { + stop_condition.RequestStop(/*exit_code=*/1234, + "stop request from thread 1"); + } + }); + requesters.Schedule([&] { + while (!stop_testing) { + stop_condition.RequestStop(/*exit_code=*/5678, + "stop request from thread 2"); + } + }); + bool got_stop_request_from_thread_1 = false; + bool got_stop_request_from_thread_2 = false; + const absl::Time start = absl::Now(); + while (absl::Now() - start < absl::Seconds(3)) { + StopCondition::StopRequest stop_request; + if (stop_condition.StopRequested(&stop_request)) { + ASSERT_THAT( + stop_request, + AnyOf(AllOf(Field(&StopCondition::StopRequest::exit_code, 1234), + Field(&StopCondition::StopRequest::reason, + "stop request from thread 1")), + AllOf(Field(&StopCondition::StopRequest::exit_code, 5678), + Field(&StopCondition::StopRequest::reason, + "stop request from thread 2")))); + if (stop_request.exit_code == 1234) { + got_stop_request_from_thread_1 = true; + } else { + got_stop_request_from_thread_2 = true; + } + stop_condition.ClearStopRequest(); + } + } + stop_testing = true; + EXPECT_TRUE(got_stop_request_from_thread_1); + EXPECT_TRUE(got_stop_request_from_thread_2); + // Requester threads would be joined at the end, so if they got stuck the test + // would time out. +} + +} // namespace +} // namespace fuzztest::internal
diff --git a/centipede/testing/fuzz_target_with_custom_mutator.cc b/centipede/testing/fuzz_target_with_custom_mutator.cc index dcd3735..babca79 100644 --- a/centipede/testing/fuzz_target_with_custom_mutator.cc +++ b/centipede/testing/fuzz_target_with_custom_mutator.cc
@@ -24,8 +24,8 @@ #include "./common/defs.h" ABSL_FLAG(bool, simulate_failure, false, - "If true, the binary will return EXIT_FAILURE to simulate a " - "failure."); + "If true, the binary will exit with EXIT_FAILURE in the custom " + "mutator to simulate a failure."); using fuzztest::internal::ByteSpan; using fuzztest::internal::MutantRef; @@ -39,6 +39,10 @@ void* Mutate(void* input, const fuzztest::internal::ExecutionMetadata& metadata) override { + if (absl::GetFlag(FLAGS_simulate_failure)) { + std::exit(EXIT_FAILURE); + } + const auto* ba = reinterpret_cast<const fuzztest::internal::ByteArray*>(input); return reinterpret_cast<void*>(new fuzztest::internal::ByteArray{*ba}); @@ -64,9 +68,6 @@ int main(int argc, char** absl_nonnull argv) { absl::ParseCommandLine(argc, argv); - if (absl::GetFlag(FLAGS_simulate_failure)) { - return EXIT_FAILURE; - } CustomMutatorRunnerCallbacks runner_callbacks; return fuzztest::internal::RunnerMain(argc, argv, runner_callbacks); }
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc index f13801b..c74427e 100644 --- a/fuzztest/internal/centipede_adaptor.cc +++ b/fuzztest/internal/centipede_adaptor.cc
@@ -362,19 +362,21 @@ sigemptyset(&new_sigact.sa_mask); new_sigact.sa_handler = [](int signum) { Runtime::instance().SetTerminationRequested(); - global_stop_condition.RequestEarlyStop(EXIT_SUCCESS); const int fd = GetStderrFdDup() != -1 ? GetStderrFdDup() : STDERR_FILENO; if (signum == SIGTERM) { + global_stop_condition.RequestStop(EXIT_SUCCESS, "SIGTERM received"); constexpr char kMsg[] = "\n[!] SIGTERM received - stopping fuzzing.\n"; write(fd, kMsg, sizeof(kMsg) - 1); return; } else if (signum == SIGHUP) { + global_stop_condition.RequestStop(EXIT_SUCCESS, "SIGHUP received"); constexpr char kMsg[] = "\n[!] SIGHUP received - stopping fuzzing.\n"; write(fd, kMsg, sizeof(kMsg) - 1); return; } else if (signum == SIGINT) { + global_stop_condition.RequestStop(EXIT_SUCCESS, "SIGINT received"); constexpr char kMsg[] = "\n[!] SIGINT received - stopping fuzzing.\n"; write(fd, kMsg, sizeof(kMsg) - 1); return; @@ -438,7 +440,7 @@ << "Termination status must be Exited if not Signaled"; return static_cast<int>(std::get<ExitCodeT>(status.Status())); } - global_stop_condition.ClearEarlyStopRequest(); + global_stop_condition.ClearStopRequest(); global_stop_condition.SetStopTime(absl::InfiniteFuture()); static absl::NoDestructor<DefaultCallbacksFactory<CentipedeDefaultCallbacks>> factory;