Add support for FUZZTEST_DISABLE_SIGNAL_HANDLERS to avoid issues with fuzzers that do their own signal handling PiperOrigin-RevId: 1000424847
diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc index 943eb72..1451859 100644 --- a/e2e_tests/functional_test.cc +++ b/e2e_tests/functional_test.cc
@@ -88,17 +88,25 @@ return env; } -void ExpectTargetAbort(TerminationStatus status, absl::string_view std_err) { +// Expects that the code under test was killed by `signum`. In Centipede mode +// the code under test runs in a runner subprocess, so the controller reports +// the runner's termination signal instead of being killed by it. +void ExpectTargetKilledBySignal(TerminationStatus status, + absl::string_view std_err, int signum) { #ifdef FUZZTEST_USE_CENTIPEDE EXPECT_THAT(status, Ne(ExitCode(0))); - EXPECT_TRUE(RE2::PartialMatch(std_err, - absl::StrCat("[Ee]xit code\\s*:\\s*", SIGABRT))) + EXPECT_TRUE( + RE2::PartialMatch(std_err, absl::StrCat("[Ee]xit code\\s*:\\s*", signum))) << std_err; #else - EXPECT_THAT(status, Eq(Signal(SIGABRT))); + EXPECT_THAT(status, Eq(Signal(signum))); #endif } +void ExpectTargetAbort(TerminationStatus status, absl::string_view std_err) { + ExpectTargetKilledBySignal(status, std_err, SIGABRT); +} + int CountSubstrs(absl::string_view haystack, absl::string_view needle) { int count = 0; while (true) { @@ -599,6 +607,65 @@ EXPECT_THAT(status, Eq(ExitCode(0))); } +TEST_F(UnitTestModeTest, ReportsCrashWhenSignalHandlerIsNotDisabled) { + auto [status, std_out, std_err] = + Run("MySuite.Aborts", kDefaultTargetBinary, + /*env=*/{{"FUZZTEST_DISABLE_SIGNAL_HANDLERS", ""}}); + ExpectTargetAbort(status, std_err); + EXPECT_THAT_LOG(std_err, HasSubstr("BUG FOUND!")); +} + +TEST_F(UnitTestModeTest, DoesNotReportCrashWhenSignalHandlerIsDisabled) { + auto [status, std_out, std_err] = + Run("MySuite.Aborts", kDefaultTargetBinary, + /*env=*/{{"FUZZTEST_DISABLE_SIGNAL_HANDLERS", "SIGABRT"}}); + ExpectTargetAbort(status, std_err); + EXPECT_THAT_LOG(std_err, Not(HasSubstr("BUG FOUND!"))); +} + +TEST_F(UnitTestModeTest, HandlesTerminationSignalByDefault) { + auto [status, std_out, std_err] = Run("MySuite.RaisesSigterm"); + EXPECT_THAT(status, Eq(ExitCode(0))); +} + +TEST_F(UnitTestModeTest, DoesNotHandleTerminationSignalWhenDisabled) { + auto [status, std_out, std_err] = + Run("MySuite.RaisesSigterm", kDefaultTargetBinary, + /*env=*/{{"FUZZTEST_DISABLE_SIGNAL_HANDLERS", "SIGTERM"}}); + ExpectTargetKilledBySignal(status, std_err, SIGTERM); +} + +// Without FUZZTEST_DISABLE_SIGNAL_HANDLERS, FuzzTest's handler runs before the +// target's pre-existing fault handler and reports the fault as a crash, even +// though the target's handler recovers from it. +TEST_F(UnitTestModeTest, + ReportsFaultBeforeTargetFaultHandlerRecoversByDefault) { + auto [status, std_out, std_err] = + Run("MySuite.RecoversFromExpectedFault", kDefaultTargetBinary, + /*env=*/{{"INSTALL_RECOVERING_FAULT_HANDLER", "1"}}); +#ifdef FUZZTEST_USE_CENTIPEDE + // The Centipede controller stops the runner after the report and echoes the + // runner's log in its crash log, so the report appears more than once. + EXPECT_THAT_LOG(std_err, HasSubstr("BUG FOUND!")); +#else + // Only the first access faults, so the crash is reported exactly once. + EXPECT_EQ(CountSubstrs(std_err, "BUG FOUND!"), 1) << std_err; +#endif + EXPECT_THAT_LOG(std_err, HasSubstr("<<Recovered from expected fault>>")); +} + +TEST_F(UnitTestModeTest, + TargetFaultHandlerRecoversWhenFuzzTestHandlersAreDisabled) { + auto [status, std_out, std_err] = + Run("MySuite.RecoversFromExpectedFault", kDefaultTargetBinary, + /*env=*/ + {{"INSTALL_RECOVERING_FAULT_HANDLER", "1"}, + {"FUZZTEST_DISABLE_SIGNAL_HANDLERS", "SIGSEGV,SIGBUS"}}); + EXPECT_THAT(status, Eq(ExitCode(0))) << std_err; + EXPECT_THAT_LOG(std_err, HasSubstr("<<Recovered from expected fault>>")); + EXPECT_THAT_LOG(std_err, Not(HasSubstr("BUG FOUND!"))); +} + void ExpectStackLimitExceededMessage(absl::string_view std_err, size_t limit_bytes) { #ifdef FUZZTEST_USE_CENTIPEDE @@ -841,6 +908,25 @@ EXPECT_THAT_LOG(std_err, HasSubstr("will not limit fuzzing runs")); } +// The timeout sends SIGTERM to the top-level process, which is the Centipede +// controller in Centipede mode. +TEST_F(FuzzingModeCommandLineInterfaceTest, + StopsGracefullyOnExternalSigtermByDefault) { + auto [status, std_out, std_err] = + RunWith({{"fuzz", "MySuite.PassesWithPositiveInput"}}, /*env=*/{}, + /*timeout=*/absl::Seconds(10)); + EXPECT_THAT(status, Eq(ExitCode(0))) << std_err; +} + +TEST_F(FuzzingModeCommandLineInterfaceTest, + IsKilledByExternalSigtermWhenSignalHandlerIsDisabled) { + auto [status, std_out, std_err] = + RunWith({{"fuzz", "MySuite.PassesWithPositiveInput"}}, + /*env=*/{{"FUZZTEST_DISABLE_SIGNAL_HANDLERS", "SIGTERM"}}, + /*timeout=*/absl::Seconds(10)); + EXPECT_THAT(status, Eq(Signal(SIGTERM))) << std_err; +} + TEST_F(FuzzingModeCommandLineInterfaceTest, LimitsFuzzingRunsWhenEnvVarIsSet) { auto [status, std_out, std_err] = RunWith({{"fuzz", "MySuite.PassesWithPositiveInput"}},
diff --git a/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc b/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc index 0a066d0..914cc82 100644 --- a/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc +++ b/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc
@@ -84,6 +84,9 @@ } FUZZ_TEST(MySuite, Aborts); +void RaisesSigterm(int) { std::raise(SIGTERM); } +FUZZ_TEST(MySuite, RaisesSigterm); + void PassesString(const std::string& v) { absl::FPrintF(stderr, "==<<Saw size=%zu>>==\n", v.size()); }
diff --git a/e2e_tests/testdata/fuzz_tests_using_googletest.cc b/e2e_tests/testdata/fuzz_tests_using_googletest.cc index 17a9a0f..463ff24 100644 --- a/e2e_tests/testdata/fuzz_tests_using_googletest.cc +++ b/e2e_tests/testdata/fuzz_tests_using_googletest.cc
@@ -17,6 +17,10 @@ // Used by `functional_test` only. We separate these into a different .cc file // to show that regular FUZZ_TEST work without having to #include GoogleTest. +#include <sys/mman.h> +#include <unistd.h> + +#include <csignal> #include <cstdio> #include <cstdlib> @@ -45,6 +49,64 @@ testing::Environment* const global_environment = testing::AddGlobalTestEnvironment(new GlobalEnvironment); +// Emulates targets (e.g., V8's Wasm trap handler) that install their own +// SIGSEGV/SIGBUS handler before FuzzTest does and recover from faults they +// expect. The handler only recovers from faults on `guard_page`, by making the +// page accessible so that the faulting access is retried and succeeds. Any +// other fault falls back to the default action. Both signals are handled +// because some platforms (e.g., macOS) may deliver this fault as SIGBUS. +void* guard_page = nullptr; + +void RecoveringFaultHandler(int signum, siginfo_t* info, void*) { + if (info->si_addr == guard_page && + mprotect(guard_page, getpagesize(), PROT_READ | PROT_WRITE) == 0) { + constexpr char kMsg[] = "<<Recovered from expected fault>>\n"; + write(STDERR_FILENO, kMsg, sizeof(kMsg) - 1); + return; // The faulting access is retried and now succeeds. + } + signal(signum, SIG_DFL); // Retrying the access now crashes. +} + +// Installs `RecoveringFaultHandler` if INSTALL_RECOVERING_FAULT_HANDLER is set. +// Global environments are set up before any test runs, and therefore before +// FuzzTest installs its own signal handlers. +class RecoveringFaultHandlerEnvironment : public testing::Environment { + public: + void SetUp() override { + if (std::getenv("INSTALL_RECOVERING_FAULT_HANDLER") == nullptr) { + return; + } + guard_page = mmap(nullptr, getpagesize(), PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (guard_page == MAP_FAILED) { + perror("mmap"); + std::abort(); + } + struct sigaction action = {}; + sigemptyset(&action.sa_mask); + action.sa_sigaction = RecoveringFaultHandler; + action.sa_flags = SA_SIGINFO; + if (sigaction(SIGSEGV, &action, nullptr) != 0 || + sigaction(SIGBUS, &action, nullptr) != 0) { + perror("sigaction"); + std::abort(); + } + } +}; + +testing::Environment* const recovering_fault_handler_environment = + testing::AddGlobalTestEnvironment(new RecoveringFaultHandlerEnvironment); + +// Writes to `guard_page`, which faults on the first run only and is recovered +// by `RecoveringFaultHandler`. Does nothing if the handler is not installed. +void RecoversFromExpectedFault(int) { + if (guard_page == nullptr) { + return; + } + *static_cast<volatile char*>(guard_page) = 1; +} +FUZZ_TEST(MySuite, RecoversFromExpectedFault); + void GoogleTestExpect(int a) { // Make sure this will fail. EXPECT_EQ(a, 42);
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc index 5885eaa..588d0e0 100644 --- a/fuzztest/internal/centipede_adaptor.cc +++ b/fuzztest/internal/centipede_adaptor.cc
@@ -358,7 +358,20 @@ void InstallCentipedeTerminationHandler() { [[maybe_unused]] static bool install_once = [] { - for (int signum : {SIGTERM, SIGHUP, SIGINT}) { + if (const char* disabled = std::getenv("FUZZTEST_DISABLE_SIGNAL_HANDLERS"); + disabled != nullptr) { + absl::FPrintF(GetStderr(), "[*] FUZZTEST_DISABLE_SIGNAL_HANDLERS=%s\n", + disabled); + } + + constexpr std::pair<int, absl::string_view> kTerminationSignals[] = { + {SIGTERM, "SIGTERM"}, {SIGHUP, "SIGHUP"}, {SIGINT, "SIGINT"}}; + for (const auto& [signum, signame] : kTerminationSignals) { + if (IsSignalHandlerDisabled(signame)) { + absl::FPrintF(GetStderr(), "[*] Not installing handlers for: %s\n", + signame); + continue; + } struct sigaction new_sigact = {}; sigemptyset(&new_sigact.sa_mask); new_sigact.sa_handler = [](int signum) {
diff --git a/fuzztest/internal/runtime.cc b/fuzztest/internal/runtime.cc index 5df7ea7..574e7f7 100644 --- a/fuzztest/internal/runtime.cc +++ b/fuzztest/internal/runtime.cc
@@ -51,6 +51,7 @@ #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" #include "absl/strings/str_replace.h" +#include "absl/strings/str_split.h" #include "absl/strings/string_view.h" #include "absl/strings/strip.h" #include "absl/time/clock.h" @@ -514,6 +515,19 @@ watchdog_spinlock_.Unlock(); } +bool IsSignalHandlerDisabled(absl::string_view signame) { + const char* disabled = std::getenv("FUZZTEST_DISABLE_SIGNAL_HANDLERS"); + if (disabled == nullptr) { + return false; + } + for (absl::string_view name : absl::StrSplit(disabled, ',')) { + if (name == signame) { + return true; + } + } + return false; +} + #if defined(__linux__) || defined(__APPLE__) struct OldSignalHandler { @@ -641,11 +655,27 @@ [](auto...) { Runtime::instance().PrintReport(&signal_out_sink); }); #endif + if (const char* disabled = std::getenv("FUZZTEST_DISABLE_SIGNAL_HANDLERS"); + disabled != nullptr) { + absl::FPrintF(GetStderr(), "[*] FUZZTEST_DISABLE_SIGNAL_HANDLERS=%s\n", + disabled); + } + for (OldSignalHandler& h : crash_handlers) { + if (IsSignalHandlerDisabled(h.signame)) { + absl::FPrintF(GetStderr(), "[*] Not installing handlers for: %s\n", + h.signame); + continue; + } SetNewSigAction(h.signum, &HandleCrash, &h.action); } for (OldSignalHandler& h : termination_handlers) { + if (IsSignalHandlerDisabled(h.signame)) { + absl::FPrintF(GetStderr(), "[*] Not installing handlers for: %s\n", + h.signame); + continue; + } SetNewSigAction(h.signum, &HandleTermination, nullptr); } }
diff --git a/fuzztest/internal/runtime.h b/fuzztest/internal/runtime.h index 1e0cbdd..e8163e3 100644 --- a/fuzztest/internal/runtime.h +++ b/fuzztest/internal/runtime.h
@@ -109,6 +109,16 @@ void InstallSignalHandlers(FILE* report_out); +// Returns true if FuzzTest should not install its handler for `signame` (e.g. +// "SIGSEGV"), as requested by the comma-separated list of signal names in the +// FUZZTEST_DISABLE_SIGNAL_HANDLERS environment variable. This is useful for +// targets that install their own handlers for signals they expect and recover +// from. +// +// This only covers FuzzTest's runtime and Centipede controller handlers and not +// the Centipede fork server handlers. +bool IsSignalHandlerDisabled(absl::string_view signame); + void InstallUnexpectedExitHandler(); // A function that is called when crash metadata is available.