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.