Crash Centipede on setup failure and prune old crash inputs with setup failures.

Note: We currently don't crash on setup failures that involve exceeding resource
limits, e.g., timeouts or OOMs. Doing that requires connecting the Centipede
watchdog with the FuzzTest runtime to be able to pass information about the
crash type (currently the watchdog directly dumps the crash type to a file and
has no way of knowing whether we're in the test setup or executing an input).
PiperOrigin-RevId: 722744618
diff --git a/centipede/centipede.cc b/centipede/centipede.cc
index 4a05539..a5b1834 100644
--- a/centipede/centipede.cc
+++ b/centipede/centipede.cc
@@ -818,31 +818,43 @@
                             const std::vector<ByteArray> &input_vec,
                             const BatchResult &batch_result) {
   CHECK_EQ(input_vec.size(), batch_result.results().size());
+
+  const size_t suspect_input_idx = std::clamp<size_t>(
+      batch_result.num_outputs_read(), 0, input_vec.size() - 1);
+  auto log_execution_failure = [&](std::string_view log_prefix) {
+    LOG(INFO) << log_prefix << "Batch execution failed:"
+              << "\nBinary               : " << binary
+              << "\nExit code            : " << batch_result.exit_code()
+              << "\nFailure              : "
+              << batch_result.failure_description()
+              << "\nNumber of inputs     : " << input_vec.size()
+              << "\nNumber of inputs read: " << batch_result.num_outputs_read()
+              << (batch_result.IsSetupFailure()
+                      ? ""
+                      : absl::StrCat("\nSuspect input index  : ",
+                                     suspect_input_idx))
+              << "\nCrash log            :\n\n";
+    for (const auto &log_line :
+         absl::StrSplit(absl::StripAsciiWhitespace(batch_result.log()), '\n')) {
+      LOG(INFO).NoPrefix() << "CRASH LOG: " << log_line;
+    }
+    LOG(INFO).NoPrefix() << "\n";
+  };
+
+  if (batch_result.IsSetupFailure()) {
+    log_execution_failure("Test Setup Failure: ");
+    LOG(FATAL) << "Terminating Centipede due to setup failure in the test.";
+  }
+
   // Skip reporting only if RequestEarlyStop is called with a failure exit code.
   // Still report if time runs out.
   if (ShouldStop() && ExitCode() != 0) return;
 
   if (++num_crashes_ > env_.max_num_crash_reports) return;
 
-  const size_t suspect_input_idx = std::clamp<size_t>(
-      batch_result.num_outputs_read(), 0, input_vec.size() - 1);
-
   const std::string log_prefix =
       absl::StrCat("ReportCrash[", num_crashes_, "]: ");
-
-  LOG(INFO) << log_prefix << "Batch execution failed:"
-            << "\nBinary               : " << binary
-            << "\nExit code            : " << batch_result.exit_code()
-            << "\nFailure              : " << batch_result.failure_description()
-            << "\nNumber of inputs     : " << input_vec.size()
-            << "\nNumber of inputs read: " << batch_result.num_outputs_read()
-            << "\nSuspect input index  : " << suspect_input_idx
-            << "\nCrash log            :\n\n";
-  for (const auto &log_line :
-       absl::StrSplit(absl::StripAsciiWhitespace(batch_result.log()), '\n')) {
-    LOG(INFO).NoPrefix() << "CRASH LOG: " << log_line;
-  }
-  LOG(INFO).NoPrefix() << "\n";
+  log_execution_failure(log_prefix);
 
   LOG_IF(INFO, num_crashes_ == env_.max_num_crash_reports)
       << log_prefix
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc
index 1c584f8..7c0784d 100644
--- a/centipede/centipede_interface.cc
+++ b/centipede/centipede_interface.cc
@@ -306,10 +306,10 @@
     const bool is_reproducible = !scoped_callbacks.callbacks()->Execute(
         env.binary, {crashing_input}, batch_result);
     const bool is_duplicate =
-        is_reproducible &&
+        is_reproducible && !batch_result.IsSetupFailure() &&
         !remaining_crash_metadata.insert(batch_result.failure_description())
              .second;
-    if (!is_reproducible || is_duplicate) {
+    if (!is_reproducible || batch_result.IsSetupFailure() || is_duplicate) {
       CHECK_OK(RemotePathDelete(crashing_input_file, /*recursively=*/false));
     } else {
       CHECK_OK(RemotePathTouchExistingFile(crashing_input_file));
diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc
index 44c612e..c0a4882 100644
--- a/centipede/centipede_test.cc
+++ b/centipede/centipede_test.cc
@@ -993,4 +993,39 @@
   EXPECT_EQ(batch_result.failure_description(), "stack-limit-exceeded");
 }
 
+namespace {
+
+class SetupFailureCallbacks : public CentipedeCallbacks {
+ public:
+  using CentipedeCallbacks::CentipedeCallbacks;
+
+  bool Execute(std::string_view binary, const std::vector<ByteArray> &inputs,
+               BatchResult &batch_result) override {
+    batch_result.ClearAndResize(inputs.size());
+    batch_result.exit_code() = EXIT_FAILURE;
+    batch_result.failure_description() = "SETUP FAILURE: something went wrong";
+    return false;
+  }
+
+  void Mutate(const std::vector<MutationInputRef> &inputs, size_t num_mutants,
+              std::vector<ByteArray> &mutants) override {
+    mutants.resize(num_mutants, {0});
+  }
+};
+
+}  // namespace
+
+TEST(Centipede, AbortsOnSetupFailure) {
+  TempDir temp_dir{test_info_->name()};
+  Environment env;
+  env.log_level = 0;  // Disable most of the logging in the test.
+  env.workdir = temp_dir.path();
+  env.batch_size = 7;            // Just some small number.
+  env.require_pc_table = false;  // No PC table here.
+  SetupFailureCallbacks mock(env);
+  MockFactory factory(mock);
+  EXPECT_DEATH(CentipedeMain(env, factory),
+               "Terminating Centipede due to setup failure in the test.");
+}
+
 }  // namespace centipede
diff --git a/centipede/runner_result.cc b/centipede/runner_result.cc
index b1b6589..843e574 100644
--- a/centipede/runner_result.cc
+++ b/centipede/runner_result.cc
@@ -15,7 +15,9 @@
 #include "./centipede/runner_result.h"
 
 #include <cstdint>
+#include <cstdlib>
 #include <cstring>
+#include <string_view>
 
 #include "./centipede/execution_metadata.h"
 #include "./centipede/feature.h"
@@ -110,4 +112,11 @@
   return true;
 }
 
+bool BatchResult::IsSetupFailure() const {
+  constexpr std::string_view kSetupFailurePrefix = "SETUP FAILURE:";
+  return exit_code_ != EXIT_SUCCESS &&
+         std::string_view(failure_description_)
+                 .substr(0, kSetupFailurePrefix.size()) == kSetupFailurePrefix;
+}
+
 }  // namespace centipede
diff --git a/centipede/runner_result.h b/centipede/runner_result.h
index 4b592be..e95f4a8 100644
--- a/centipede/runner_result.h
+++ b/centipede/runner_result.h
@@ -137,6 +137,10 @@
   // When running N inputs, ClearAndResize(N) must be called before Read().
   bool Read(BlobSequence& blobseq);
 
+  // Returns true if the batch execution failed due to a setup failure, and not
+  // a crash tied to a specific input.
+  bool IsSetupFailure() const;
+
   // Accessors.
   std::vector<ExecutionResult>& results() { return results_; }
   const std::vector<ExecutionResult>& results() const { return results_; }
diff --git a/centipede/runner_result_test.cc b/centipede/runner_result_test.cc
index d67880a..56b51e6 100644
--- a/centipede/runner_result_test.cc
+++ b/centipede/runner_result_test.cc
@@ -15,6 +15,7 @@
 #include "./centipede/runner_result.h"
 
 #include <cstdint>
+#include <cstdlib>
 #include <filesystem>  // NOLINT
 #include <fstream>
 #include <ios>
@@ -157,5 +158,13 @@
                                    ));
 }
 
+TEST(ExecutionResult, IdentifiesSetupFailure) {
+  BatchResult batch_result;
+  batch_result.exit_code() = EXIT_FAILURE;
+  batch_result.failure_description() = "SETUP FAILURE: something went wrong";
+
+  EXPECT_TRUE(batch_result.IsSetupFailure());
+}
+
 }  // namespace
 }  // namespace centipede
diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc
index b263b4a..1351af1 100644
--- a/e2e_tests/functional_test.cc
+++ b/e2e_tests/functional_test.cc
@@ -1821,6 +1821,23 @@
 }
 
 TEST_P(FuzzingModeCrashFindingTest,
+       SetupFailureCrashMetadataIsDumpedIfEnvVarIsSet) {
+  if (GetParam() == ExecutionModelParam::kSingleBinary) {
+    // TODO(b/393582695): Reconsider how we want to handle setup failures in the
+    // single-binary mode.
+    GTEST_SKIP() << "Currently not supported in single-binary mode.";
+  }
+  TempDir out_dir;
+  const std::string crash_metadata_path = out_dir.path() / "crash_metadata";
+  auto [status, std_out, std_err] =
+      Run("FaultySetupTest.NoOp", kDefaultTargetBinary,
+          {{"FUZZTEST_CRASH_METADATA_PATH", crash_metadata_path}});
+
+  EXPECT_THAT(ReadFile(crash_metadata_path),
+              Optional(Eq("SETUP FAILURE: SIGABRT")));
+}
+
+TEST_P(FuzzingModeCrashFindingTest,
        CustomMutatorAndMutateCalllbackWorksForLLVMFuzzer) {
   TempDir out_dir;
   auto [status, std_out, std_err] =
diff --git a/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc b/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc
index 5b661e3..8049505 100644
--- a/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc
+++ b/e2e_tests/testdata/fuzz_tests_for_functional_testing.cc
@@ -840,4 +840,11 @@
 // when initializing the corpus for fuzzing. So we provide one.
 FUZZ_TEST(MySuite, SkipInputs).WithSeeds({1});
 
+class FaultySetupTest {
+ public:
+  FaultySetupTest() { std::abort(); }
+  void NoOp(int) {}
+};
+FUZZ_TEST_F(FaultySetupTest, NoOp);
+
 }  // namespace
diff --git a/fuzztest/internal/runtime.cc b/fuzztest/internal/runtime.cc
index f77f06a..fb47977 100644
--- a/fuzztest/internal/runtime.cc
+++ b/fuzztest/internal/runtime.cc
@@ -273,7 +273,10 @@
   if (crash_handler_hook) crash_handler_hook();
 
   for (CrashMetadataListenerRef listener : crash_metadata_listeners_) {
-    listener(crash_type_.value_or("Generic crash"), {});
+    const std::string final_crash_type =
+        absl::StrCat(current_args_ == nullptr ? "SETUP FAILURE: " : "",
+                     crash_type_.value_or("Generic crash"));
+    listener(final_crash_type, {});
   }
 
   if (run_mode() != RunMode::kUnitTest) {