When in fuzzing mode, if a non-fatal failure is found (eg like a failed
EXPECT_EQ) try to minimize the sample in-process before triggering a report.

PiperOrigin-RevId: 506363317
diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc
index 3af3604..e4b5694 100644
--- a/e2e_tests/functional_test.cc
+++ b/e2e_tests/functional_test.cc
@@ -1249,5 +1249,15 @@
       1, CountSubstrs(std_err, "<<CallCountGoogleTest::TearDownTestSuite()>>"));
 }
 
+TEST_F(FuzzingModeTest, NonFatalFailureAllowsMinimization) {
+  auto [status, std_out, std_err] =
+      RunWith("--fuzz=MySuite.NonFatalFailureAllowsMinimization");
+  // The final failure should be with the known minimal result, even though many
+  // "larger" inputs also trigger the failure.
+  EXPECT_THAT(std_err, HasSubstr("argument 0: \"0123\""));
+
+  EXPECT_THAT(status.Signal(), Eq(SIGABRT));
+}
+
 }  // namespace
 }  // namespace fuzztest::internal
diff --git a/e2e_tests/testdata/fuzz_tests_using_googletest.cc b/e2e_tests/testdata/fuzz_tests_using_googletest.cc
index 9f799af..86ca808 100644
--- a/e2e_tests/testdata/fuzz_tests_using_googletest.cc
+++ b/e2e_tests/testdata/fuzz_tests_using_googletest.cc
@@ -23,6 +23,7 @@
 #include "gtest/gtest.h"
 #include "./fuzztest/fuzztest.h"
 #include "./fuzztest/googletest_fixture_adapter.h"
+#include "./fuzztest/internal/test_protobuf.pb.h"
 
 namespace {
 
@@ -106,4 +107,15 @@
 void WorksAsFuzzTest(int) {}
 FUZZ_TEST(SharedSuite, WorksAsFuzzTest);
 
+void NonFatalFailureAllowsMinimization(const std::string& str) {
+  // Make very fuzz predicate that would fail on a large number of values, but
+  // there is one very specific minimum.
+  if (str.size() < 4 || str[0] < '0' || str[1] < '1' || str[2] < '2' ||
+      str[3] <= str[2]) {
+    return;
+  }
+  ADD_FAILURE() << str;
+}
+FUZZ_TEST(MySuite, NonFatalFailureAllowsMinimization);
+
 }  // namespace
diff --git a/fuzztest/fuzztest_gtest_main.cc b/fuzztest/fuzztest_gtest_main.cc
index cc57696..984bac4 100644
--- a/fuzztest/fuzztest_gtest_main.cc
+++ b/fuzztest/fuzztest_gtest_main.cc
@@ -64,7 +64,7 @@
   const bool is_duration_specified =
       absl::ZeroDuration() < duration && duration < absl::InfiniteDuration();
   if (is_duration_specified) {
-    fuzztest::internal::fuzz_time_limit = duration;
+    fuzztest::internal::Runtime::instance().SetFuzzTimeLimit(duration);
   }
   if (is_fuzz_specified || is_duration_specified) {
     GOOGLEFUZZTEST_REGISTER_FOR_GOOGLETEST(fuzztest::RunMode::kFuzz, &argc,
diff --git a/fuzztest/googletest_adaptor.h b/fuzztest/googletest_adaptor.h
index ad93fbc..9b57d80 100644
--- a/fuzztest/googletest_adaptor.h
+++ b/fuzztest/googletest_adaptor.h
@@ -43,7 +43,7 @@
    ::testing::UnitTest::GetInstance()->listeners().Append(                    \
        new ::fuzztest::internal::GTest_EventListener<                         \
            ::testing::EmptyTestEventListener, ::testing::TestPartResult>()),  \
-   ::fuzztest::internal::run_mode = selected_run_mode)
+   ::fuzztest::internal::Runtime::instance().SetRunMode(selected_run_mode))
 
 namespace fuzztest::internal {
 
@@ -54,7 +54,7 @@
 
   void TestBody() override {
     auto test = test_.make();
-    if (run_mode == RunMode::kUnitTest) {
+    if (Runtime::instance().run_mode() == RunMode::kUnitTest) {
       test->RunInUnitTestMode();
     } else {
       ASSERT_EQ(0, test->RunInFuzzingMode(argc_, argv_)) << "Fuzzing failure.";
@@ -84,14 +84,17 @@
  public:
   void OnTestPartResult(const TestPartResult& test_part_result) override {
     if (!test_part_result.failed()) return;
-    if (run_mode == RunMode::kFuzz) {
-      // The SIGABRT will trigger a report.
-      std::abort();
+    Runtime& runtime = Runtime::instance();
+    if (runtime.run_mode() == RunMode::kFuzz) {
+      if (runtime.should_terminate_on_non_fatal_failure()) {
+        // The SIGABRT will trigger a report.
+        std::abort();
+      }
     } else {
       // Otherwise, we report it manually.
-      on_failure.PrintReportOnDefaultSink();
+      runtime.PrintReportOnDefaultSink();
     }
-    external_failure_was_detected.store(true, std::memory_order_relaxed);
+    runtime.SetExternalFailureDetected(true);
   }
 };
 
diff --git a/fuzztest/internal/domain.h b/fuzztest/internal/domain.h
index 859e06b..f2e8d4a 100644
--- a/fuzztest/internal/domain.h
+++ b/fuzztest/internal/domain.h
@@ -990,7 +990,8 @@
     const bool can_shrink = val.size() > this->min_size_;
     const bool can_grow = !only_shrink && val.size() < this->max_size_;
     const bool can_change = val.size() != 0;
-    const bool can_use_memory_dict = container_has_memory_dict && can_change &&
+    const bool can_use_memory_dict = !only_shrink &&
+                                     container_has_memory_dict && can_change &&
                                      GetExecutionCoverage() != nullptr;
 
     const int action_count =
diff --git a/fuzztest/internal/runtime.cc b/fuzztest/internal/runtime.cc
index c0b3100..c9c3ccd 100644
--- a/fuzztest/internal/runtime.cc
+++ b/fuzztest/internal/runtime.cc
@@ -61,14 +61,9 @@
 
 namespace fuzztest::internal {
 
-RunMode run_mode = RunMode::kUnitTest;
-ABSL_CONST_INIT absl::Duration fuzz_time_limit = absl::InfiniteDuration();
-std::atomic<bool> external_failure_was_detected;
-std::atomic<bool> termination_requested;
-OnFailure on_failure;
 void (*crash_handler_hook)();
 
-void OnFailure::DumpReproducer(std::string_view outdir) const {
+void Runtime::DumpReproducer(std::string_view outdir) const {
   const std::string content =
       current_args_->domain.UntypedSerializeCorpus(current_args_->corpus_value)
           .ToString();
@@ -82,7 +77,7 @@
   }
 }
 
-void OnFailure::PrintFinalStats(absl::FormatRawSink out) const {
+void Runtime::PrintFinalStats(absl::FormatRawSink out) const {
   const std::string separator = '\n' + std::string(65, '=') + '\n';
   absl::Format(out, "%s=== Fuzzing stats\n\n", separator);
 
@@ -95,10 +90,10 @@
   absl::Format(out, "Corpus size: %d\n", stats_->useful_inputs);
 }
 
-void OnFailure::PrintReport(absl::FormatRawSink out) const {
+void Runtime::PrintReport(absl::FormatRawSink out) const {
   // We don't want to try and print a fuzz report when we are not running a fuzz
   // test, even if we got a crash.
-  if (!enabled_) return;
+  if (!reporter_enabled_) return;
 
   if (crash_handler_hook) crash_handler_hook();
 
@@ -110,7 +105,7 @@
     }
   }
 
-  if (run_mode != RunMode::kUnitTest) {
+  if (run_mode() != RunMode::kUnitTest) {
     PrintFinalStats(out);
   }
 
@@ -118,8 +113,9 @@
 
   if (current_args_ != nullptr) {
     absl::Format(out, "%s=== BUG FOUND!\n\n", separator);
-    absl::Format(out, "%s:%d: Counterexample found for %s.%s.\n", test_->file(),
-                 test_->line(), test_->suite_name(), test_->test_name());
+    absl::Format(out, "%s:%d: Counterexample found for %s.%s.\n",
+                 current_test_->file(), current_test_->line(),
+                 current_test_->suite_name(), current_test_->test_name());
     absl::Format(out, "The test fails with input:\n");
     const int num_args = current_args_->domain.UntypedPrintCorpusValue(
         current_args_->corpus_value, out, PrintMode::kHumanReadable, -1);
@@ -133,10 +129,10 @@
 
     // There doesn't seem to be a good way to generate a reproducer test when
     // the test uses a fixture (see b/241271658).
-    if (!test_->uses_fixture()) {
+    if (!current_test_->uses_fixture()) {
       absl::Format(out, "%s=== Reproducer test\n\n", separator);
       absl::Format(out, "TEST(%1$s, %2$sRegression) {\n  %2$s(\n",
-                   test_->suite_name(), test_->test_name());
+                   current_test_->suite_name(), current_test_->test_name());
       for (size_t i = 0; i < num_args; ++i) {
         if (i != 0) absl::Format(out, ",\n");
         absl::Format(out, "    ");
@@ -148,8 +144,9 @@
     }
   } else {
     absl::Format(out, "%s=== SETUP FAILURE!\n\n", separator);
-    absl::Format(out, "%s:%d: There was a problem with %s.%s.", test_->file(),
-                 test_->line(), test_->suite_name(), test_->test_name());
+    absl::Format(out, "%s:%d: There was a problem with %s.%s.",
+                 current_test_->file(), current_test_->line(),
+                 current_test_->suite_name(), current_test_->test_name());
     if (test_abort_message != nullptr) {
       absl::Format(out, "%s", *test_abort_message);
     }
@@ -191,7 +188,7 @@
   if (!old_handler || signum != SIGTRAP ||
       (info->si_code != TRAP_PERF && info->si_code != SI_TIMER)) {
     // Dump our info first.
-    on_failure.PrintReport(&signal_out_sink);
+    Runtime::instance().PrintReport(&signal_out_sink);
     // The old signal handler might print important messages (e.g., strack
     // trace) to the original file descriptors, therefore we restore them before
     // calling them.
@@ -204,7 +201,7 @@
 }
 
 static void HandleTermination(int, siginfo_t*, void*) {
-  termination_requested.store(true, std::memory_order_relaxed);
+  Runtime::instance().SetTerminationRequested();
 }
 
 static void SetNewSigAction(int signum, void (*handler)(int, siginfo_t*, void*),
@@ -238,7 +235,7 @@
   // after printing its output. This handler helps us print our output
   // afterwards.
   __sanitizer_set_death_callback(
-      [](auto...) { on_failure.PrintReport(&signal_out_sink); });
+      [](auto...) { Runtime::instance().PrintReport(&signal_out_sink); });
 #endif
 
   for (OldSignalHandler& h : crash_handlers) {
@@ -250,11 +247,11 @@
   }
 }
 
-void OnFailure::PrintFinalStatsOnDefaultSink() const {
+void Runtime::PrintFinalStatsOnDefaultSink() const {
   PrintFinalStats(&signal_out_sink);
 }
 
-void OnFailure::PrintReportOnDefaultSink() const {
+void Runtime::PrintReportOnDefaultSink() const {
   PrintReport(&signal_out_sink);
 }
 
@@ -262,9 +259,9 @@
 // TODO(sbenzaquen): We should still install signal handlers in other systems.
 void InstallSignalHandlers(FILE* out) {}
 
-void OnFailure::PrintFinalStatsOnDefaultSink() const {}
+void Runtime::PrintFinalStatsOnDefaultSink() const {}
 
-void OnFailure::PrintReportOnDefaultSink() const {}
+void Runtime::PrintReportOnDefaultSink() const {}
 #endif  // __linux__
 
 using corpus_type = GenericDomainCorpusType;
@@ -289,7 +286,9 @@
       absl::discrete_distribution<>(weights.begin(), weights.end());
 }
 
-FuzzTestFuzzerImpl::~FuzzTestFuzzerImpl() { on_failure.Disable(); }
+FuzzTestFuzzerImpl::~FuzzTestFuzzerImpl() {
+  Runtime::instance().DisableReporter();
+}
 
 std::optional<corpus_type> FuzzTestFuzzerImpl::TryParse(std::string_view data) {
   if (auto parsed = IRObject::FromString(data)) {
@@ -299,7 +298,7 @@
 }
 
 bool FuzzTestFuzzerImpl::ReplayInputsIfAvailable() {
-  run_mode = RunMode::kFuzz;
+  runtime_.SetRunMode(RunMode::kFuzz);
 
   if (const auto replay_corpus = ReadReplayFile()) {
     for (const auto& corpus_value : *replay_corpus) {
@@ -433,6 +432,11 @@
 FuzzTestFuzzerImpl::RunResult FuzzTestFuzzerImpl::TrySample(
     const Input& sample, bool write_to_file) {
   RunResult run_result = RunOneInput(sample);
+  if (runtime_.external_failure_detected()) {
+    // We detected a non fatal failure. Record it separately to minimize it
+    // locally.
+    minimal_non_fatal_counterexample_ = sample;
+  }
   if (!run_result.new_coverage) return run_result;
 
   if (write_to_file) TryWriteCorpusFile(sample);
@@ -539,7 +543,7 @@
   if (runs_limit_.has_value() && stats_.runs >= *runs_limit_) return true;
   if (time_limit_ != absl::InfiniteFuture() && absl::Now() > time_limit_)
     return true;
-  return termination_requested.load(std::memory_order_relaxed);
+  return runtime_.termination_requested();
 }
 
 void FuzzTestFuzzerImpl::PopulateFromSeeds() {
@@ -552,8 +556,8 @@
 void FuzzTestFuzzerImpl::RunInUnitTestMode() {
   fixture_driver_->SetUpFuzzTest();
   [&] {
-    on_failure.Enable(&stats_, [] { return absl::Now(); });
-    on_failure.SetCurrentTest(&test_);
+    runtime_.EnableReporter(&stats_, [] { return absl::Now(); });
+    runtime_.SetCurrentTest(&test_);
 
     // TODO(sbenzaquen): Currently, some infrastructure code assumes that replay
     // works in unit test mode, so we support it. However, we would like to
@@ -563,11 +567,11 @@
     if (ReplayInputsIfAvailable()) {
       // If ReplayInputs returns, it means the replay didn't crash.
       // In replay mode, we only replay.
-      on_failure.Disable();
+      runtime_.DisableReporter();
       return;
     }
 
-    run_mode = RunMode::kUnitTest;
+    runtime_.SetRunMode(RunMode::kUnitTest);
 
     PopulateFromSeeds();
 
@@ -576,9 +580,9 @@
     Input mutation{params_domain_->UntypedInit(prng)};
     constexpr size_t max_iterations = 10000;
     for (int i = 0; i < max_iterations; ++i) {
-      external_failure_was_detected.store(false, std::memory_order_relaxed);
+      runtime_.SetExternalFailureDetected(false);
       RunOneInput(mutation);
-      if (external_failure_was_detected.load(std::memory_order_relaxed)) {
+      if (runtime_.external_failure_detected()) {
         break;
       }
       // We mutate the value, except that every num_mutations_per_value we
@@ -597,7 +601,7 @@
         break;
       }
     }
-    on_failure.SetCurrentTest(nullptr);
+    runtime_.SetCurrentTest(nullptr);
   }();
   fixture_driver_->TearDownFuzzTest();
 }
@@ -606,8 +610,8 @@
     const Input& input) {
   ++stats_.runs;
   auto untyped_args = params_domain_->UntypedGetValue(input.args);
-  OnFailure::Args debug_args{input.args, *params_domain_};
-  on_failure.SetCurrentArgs(&debug_args);
+  Runtime::Args debug_args{input.args, *params_domain_};
+  runtime_.SetCurrentArgs(&debug_args);
 
   // Reset and observe the coverage map and start tracing in
   // the tightest scope possible. In particular, we can't include the call
@@ -634,19 +638,58 @@
   if (execution_coverage_ != nullptr) {
     new_coverage = corpus_coverage_.Update(execution_coverage_);
   }
-  on_failure.UnsetCurrentArgs();
+  runtime_.UnsetCurrentArgs();
   return {new_coverage, run_time};
 }
 
+void FuzzTestFuzzerImpl::MinimizeNonFatalFailureLocally(absl::BitGenRef prng) {
+  // We try to minimize the counterexample until we reach a point where no new
+  // failures are found.
+  // We stop when run kMaxTriedWithoutFailure consecutive runs without finding a
+  // smaller failure, but also add a time limit in case each iteration takes too
+  // long.
+  const absl::Time deadline =
+      std::min(absl::Now() + absl::Minutes(1), time_limit_);
+  int tries_without_failure = 0;
+  constexpr int kMaxTriedWithoutFailure = 10000;
+  while (tries_without_failure < kMaxTriedWithoutFailure &&
+         absl::Now() < deadline) {
+    auto copy = *minimal_non_fatal_counterexample_;
+    // Mutate a random number of times, in case one is not enough to
+    // reach another failure, but prefer a low number of mutations (thus Zipf).
+    for (int num_mutations = absl::Zipf(prng, 10); num_mutations >= 0;
+         --num_mutations) {
+      params_domain_->UntypedMutate(copy.args, prng, /* only_shrink= */ true);
+    }
+    // Only run it if it actually is different. Random mutations might
+    // not actually change the value, or we have reached a minimum that can't be
+    // minimized anymore.
+    if (params_domain_
+            ->UntypedSerializeCorpus(minimal_non_fatal_counterexample_->args)
+            .ToString() !=
+        params_domain_->UntypedSerializeCorpus(copy.args).ToString()) {
+      runtime_.SetExternalFailureDetected(false);
+      RunOneInput(copy);
+      if (runtime_.external_failure_detected()) {
+        // Found a smaller one, record it and reset the counter.
+        minimal_non_fatal_counterexample_ = std::move(copy);
+        tries_without_failure = 0;
+        continue;
+      }
+    }
+    ++tries_without_failure;
+  }
+}
+
 int FuzzTestFuzzerImpl::RunInFuzzingMode(int* /*argc*/, char*** /*argv*/) {
   fixture_driver_->SetUpFuzzTest();
   const int exit_code = [&] {
-    run_mode = RunMode::kFuzz;
+    runtime_.SetRunMode(RunMode::kFuzz);
 
     if (IsSilenceTargetEnabled()) SilenceTargetStdoutAndStderr();
 
-    on_failure.Enable(&stats_, [] { return absl::Now(); });
-    on_failure.SetCurrentTest(&test_);
+    runtime_.EnableReporter(&stats_, [] { return absl::Now(); });
+    runtime_.SetCurrentTest(&test_);
 
     if (ReplayInputsIfAvailable()) {
       // If ReplayInputs returns, it means the replay didn't crash.
@@ -696,12 +739,13 @@
       }
     }
 
-    if (fuzz_time_limit != absl::InfiniteDuration()) {
+    if (runtime_.fuzz_time_limit() != absl::InfiniteDuration()) {
       absl::FPrintF(GetStderr(), "[.] Fuzzing timeout set to: %s\n",
-                    absl::FormatDuration(fuzz_time_limit));
-      time_limit_ = stats_.start_time + fuzz_time_limit;
+                    absl::FormatDuration(runtime_.fuzz_time_limit()));
+      time_limit_ = stats_.start_time + runtime_.fuzz_time_limit();
     }
 
+    runtime_.SetShouldTerminateOnNonFatalFailure(false);
     // Fuzz corpus elements in round robin fashion.
     while (!ShouldStop()) {
       Input input_to_mutate = [&]() -> Input {
@@ -723,11 +767,24 @@
         Input mutation = input_to_mutate;
         MutateValue(mutation, prng);
         TrySampleAndUpdateInMemoryCorpus(std::move(mutation));
+
+        if (minimal_non_fatal_counterexample_.has_value()) {
+          // We found a failure, let's minimize it here.
+          MinimizeNonFatalFailureLocally(prng);
+          // Once we have minimized enough, let it crash with the best sample we
+          // got.
+          // TODO(sbenzaquen): Consider a different approach where we don't retry
+          // the failing sample to force a crash. Instead, we could store the
+          // information from the first failure and generate a report manually.
+          runtime_.SetShouldTerminateOnNonFatalFailure(true);
+          runtime_.SetExternalFailureDetected(false);
+          RunOneInput(*minimal_non_fatal_counterexample_);
+        }
       }
     }
 
     absl::FPrintF(GetStderr(), "\n[.] Fuzzing was terminated.\n");
-    on_failure.PrintFinalStatsOnDefaultSink();
+    runtime_.PrintFinalStatsOnDefaultSink();
     absl::FPrintF(GetStderr(), "\n");
     return 0;
   }();
diff --git a/fuzztest/internal/runtime.h b/fuzztest/internal/runtime.h
index d493581..4385fce 100644
--- a/fuzztest/internal/runtime.h
+++ b/fuzztest/internal/runtime.h
@@ -113,40 +113,62 @@
 
 void InstallSignalHandlers(FILE* report_out);
 
-// Some failures are not necessarily detected by signal handlers or by
-// sanitizers. For example, we could have test framework failures like
-// `EXPECT_EQ` failures from GoogleTest.
-// If such a failure is detected, the external system can set
-// `external_failure_was_detected` to true to bubble it up.
-// Note: Even though failures should happen within the code under test, they
-// could be set from other threads at any moment. We make it an atomic to avoid
-// a race condition.
-extern std::atomic<bool> external_failure_was_detected;
-
-// If true, fuzzing should terminate as soon as possible.
-// Atomic because it is set from signal handlers.
-extern std::atomic<bool> termination_requested;
-
-extern RunMode run_mode;
-extern absl::Duration fuzz_time_limit;
-
-class OnFailure {
+// This class encapsulates the runtime state that is global by necessity.
+// The state is accessed by calling `Runtime::instance()`, which handles the
+// necessary initialization steps.
+class Runtime {
  public:
-  void Enable(const RuntimeStats* stats, absl::Time (*clock_fn)()) {
-    enabled_ = true;
+  static Runtime& instance() {
+    static auto* runtime = new Runtime();
+    return *runtime;
+  }
+
+  void SetExternalFailureDetected(bool v) {
+    external_failure_was_detected_.store(v, std::memory_order_relaxed);
+  }
+  bool external_failure_detected() const {
+    return external_failure_was_detected_.load(std::memory_order_relaxed);
+  }
+
+  void SetShouldTerminateOnNonFatalFailure(bool v) {
+    should_terminate_on_non_fatal_failure_ = v;
+  }
+
+  bool should_terminate_on_non_fatal_failure() const {
+    return should_terminate_on_non_fatal_failure_;
+  }
+
+  void SetTerminationRequested() {
+    termination_requested_.store(true, std::memory_order_relaxed);
+  }
+
+  bool termination_requested() const {
+    return termination_requested_.load(std::memory_order_relaxed);
+  }
+
+  void SetRunMode(RunMode run_mode) { run_mode_ = run_mode; }
+  RunMode run_mode() const { return run_mode_; }
+
+  void SetFuzzTimeLimit(absl::Duration fuzz_time_limit) {
+    fuzz_time_limit_ = fuzz_time_limit;
+  }
+  absl::Duration fuzz_time_limit() const { return fuzz_time_limit_; }
+
+  void EnableReporter(const RuntimeStats* stats, absl::Time (*clock_fn)()) {
+    reporter_enabled_ = true;
     stats_ = stats;
     clock_fn_ = clock_fn;
     // In case we have not installed them yet, do so now.
     InstallSignalHandlers(GetStderr());
   }
-  void Disable() { enabled_ = false; }
+  void DisableReporter() { reporter_enabled_ = false; }
 
   struct Args {
     const GenericDomainCorpusType& corpus_value;
     UntypedDomainInterface& domain;
   };
 
-  void SetCurrentTest(const FuzzTest* test) { test_ = test; }
+  void SetCurrentTest(const FuzzTest* test) { current_test_ = test; }
 
   void SetCurrentArgs(Args* args) { current_args_ = args; }
   void UnsetCurrentArgs() { current_args_ = nullptr; }
@@ -157,15 +179,38 @@
   void PrintReportOnDefaultSink() const;
 
  private:
+  Runtime() = default;
+
   void DumpReproducer(std::string_view outdir) const;
 
-  bool enabled_ = false;
-  Args* current_args_;
-  const FuzzTest* test_;
-  const RuntimeStats* stats_;
+  // Some failures are not necessarily detected by signal handlers or by
+  // sanitizers. For example, we could have test framework failures like
+  // `EXPECT_EQ` failures from GoogleTest.
+  // If such a failure is detected, the external system can set
+  // `external_failure_was_detected` to true to bubble it up.
+  // Note: Even though failures should happen within the code under test, they
+  // could be set from other threads at any moment. We make it an atomic to
+  // avoid a race condition.
+  std::atomic<bool> external_failure_was_detected_{false};
+
+  // To support in-process minimization for non-fatal failures we signal
+  // suppress termination until we believe minimization is complete.
+  bool should_terminate_on_non_fatal_failure_ = true;
+
+  // If true, fuzzing should terminate as soon as possible.
+  // Atomic because it is set from signal handlers.
+  std::atomic<bool> termination_requested_{false};
+
+  RunMode run_mode_ = RunMode::kUnitTest;
+  absl::Duration fuzz_time_limit_ = absl::InfiniteDuration();
+
+  bool reporter_enabled_ = false;
+  Args* current_args_ = nullptr;
+  const FuzzTest* current_test_ = nullptr;
+  const RuntimeStats* stats_ = nullptr;
   absl::Time (*clock_fn_)() = nullptr;
 };
-extern OnFailure on_failure;
+
 extern void (*crash_handler_hook)();
 
 template <typename Arg, size_t I, typename Tuple>
@@ -223,6 +268,8 @@
 
   void UpdateCorpusDistribution();
 
+  void MinimizeNonFatalFailureLocally(absl::BitGenRef prng);
+
   // Runs on `sample` and returns new coverage and run time. If there's new
   // coverage, outputs updated runtime stats. Additionally, if `write_to_file`
   // is true, tries to write the sample to a file.
@@ -265,6 +312,9 @@
   RuntimeStats stats_{};
   std::optional<size_t> runs_limit_;
   absl::Time time_limit_ = absl::InfiniteFuture();
+  std::optional<Input> minimal_non_fatal_counterexample_;
+
+  Runtime& runtime_ = Runtime::instance();
 
 #ifdef FUZZTEST_COMPATIBILITY_MODE
   friend class FuzzTestExternalEngineAdaptor;
diff --git a/fuzztest/internal/runtime_test.cc b/fuzztest/internal/runtime_test.cc
index 1d35316..5c0f6b4 100644
--- a/fuzztest/internal/runtime_test.cc
+++ b/fuzztest/internal/runtime_test.cc
@@ -28,9 +28,10 @@
 namespace {
 
 TEST(OnFailureTest, Output) {
-  const auto get_failure = [] {
+  auto& runtime = Runtime::instance();
+  const auto get_failure = [&] {
     std::string s;
-    on_failure.PrintReport(&s);
+    runtime.PrintReport(&s);
     return s;
   };
   // Disabled by default.
@@ -39,14 +40,14 @@
   FuzzTest test({"SUITE_NAME", "TEST_NAME", "FILE", 123}, nullptr);
   std::tuple args(17, std::string("ABC"));
   const RuntimeStats stats = {absl::FromUnixNanos(0), 1, 2, 3, 4};
-  on_failure.Enable(&stats, [] { return absl::FromUnixNanos(1979); });
-  run_mode = RunMode::kFuzz;
+  runtime.EnableReporter(&stats, [] { return absl::FromUnixNanos(1979); });
+  runtime.SetRunMode(RunMode::kFuzz);
   auto domain = TupleOf(Arbitrary<int>(), Arbitrary<std::string>());
   GenericDomainCorpusType generic_args(
       std::in_place_type<std::tuple<int, std::string>>, args);
-  OnFailure::Args debug_args{generic_args, domain};
-  on_failure.SetCurrentTest(&test);
-  on_failure.SetCurrentArgs(&debug_args);
+  Runtime::Args debug_args{generic_args, domain};
+  runtime.SetCurrentTest(&test);
+  runtime.SetCurrentArgs(&debug_args);
   EXPECT_EQ(get_failure(), R"(
 =================================================================
 === Fuzzing stats
@@ -78,7 +79,7 @@
 =================================================================
 )");
 
-  on_failure.Disable();
+  runtime.DisableReporter();
   EXPECT_EQ(get_failure(), "");
 }