[compiler-rt] [llvm] [libFuzzer] Stop fuzzing if no new corpus were found within a specified period (PR #176177)

via llvm-commits llvm-commits at lists.llvm.org
Fri Oct 2 01:29:27 PDT 2026


https://github.com/TERESH1 updated https://github.com/llvm/llvm-project/pull/176177

>From f994e60e22110ff80254c4916226655d7ddc4d54 Mon Sep 17 00:00:00 2001
From: TERESH1 <svyatoslavtereshin at yandex.ru>
Date: Fri, 2 Oct 2026 08:24:09 +0000
Subject: [PATCH] [libFuzzer] Stop fuzzing if no new paths were found within a
 specified period

This patch adds a new command-line option `-stale_corpus_timeout=N` which allows you to specify that the fuzzer should exit if it hasn't found a new corpus within a certain amount of time. It also adds to stats how many seconds a new path has not been found. An analog of `AFL_EXIT_ON_TIME`.
---
 compiler-rt/lib/fuzzer/FuzzerDriver.cpp       |  6 ++-
 compiler-rt/lib/fuzzer/FuzzerFlags.def        |  3 ++
 compiler-rt/lib/fuzzer/FuzzerFork.cpp         | 43 +++++++++++++++----
 compiler-rt/lib/fuzzer/FuzzerInternal.h       | 15 +++++--
 compiler-rt/lib/fuzzer/FuzzerLoop.cpp         |  3 ++
 compiler-rt/lib/fuzzer/FuzzerOptions.h        |  1 +
 .../fuzzer/stale_corpus_timeout-reload.test   | 15 +++++++
 .../test/fuzzer/stale_corpus_timeout.test     | 15 +++++++
 llvm/docs/LibFuzzer.md                        |  4 ++
 9 files changed, 92 insertions(+), 13 deletions(-)
 create mode 100644 compiler-rt/test/fuzzer/stale_corpus_timeout-reload.test
 create mode 100644 compiler-rt/test/fuzzer/stale_corpus_timeout.test

diff --git a/compiler-rt/lib/fuzzer/FuzzerDriver.cpp b/compiler-rt/lib/fuzzer/FuzzerDriver.cpp
index fda788020b79b..d3b885296fc44 100644
--- a/compiler-rt/lib/fuzzer/FuzzerDriver.cpp
+++ b/compiler-rt/lib/fuzzer/FuzzerDriver.cpp
@@ -698,6 +698,7 @@ int FuzzerDriver(int *argc, char ***argv, UserCallback Callback) {
   Options.IgnoreOOMs = Flags.ignore_ooms;
   Options.IgnoreCrashes = Flags.ignore_crashes;
   Options.MaxTotalTimeSec = Flags.max_total_time;
+  Options.StaleCorpusTimeoutSec = Flags.stale_corpus_timeout;
   Options.DoCrossOver = Flags.cross_over;
   Options.CrossOverUniformDist = Flags.cross_over_uniform_dist;
   Options.MutateDepth = Flags.mutate_depth;
@@ -924,8 +925,9 @@ int FuzzerDriver(int *argc, char ***argv, UserCallback Callback) {
   F->Loop(CorporaFiles);
 
   if (Flags.verbosity)
-    Printf("Done %zd runs in %zd second(s)\n", F->getTotalNumberOfRuns(),
-           F->secondsSinceProcessStartUp());
+    Printf("Done %zd runs in %zd second(s), %zd second(s) without new corpus\n",
+           F->getTotalNumberOfRuns(), F->secondsSinceProcessStartUp(),
+           F->secondsSinceLastNewCorpus());
   F->PrintFinalStats();
 
   exit(0);  // Don't let F destroy itself.
diff --git a/compiler-rt/lib/fuzzer/FuzzerFlags.def b/compiler-rt/lib/fuzzer/FuzzerFlags.def
index 9b1da6184bcc9..cb12419b93065 100644
--- a/compiler-rt/lib/fuzzer/FuzzerFlags.def
+++ b/compiler-rt/lib/fuzzer/FuzzerFlags.def
@@ -56,6 +56,9 @@ FUZZER_FLAG_INT(timeout_exitcode, 70, "When libFuzzer reports a timeout "
   "this exit code will be used.")
 FUZZER_FLAG_INT(max_total_time, 0, "If positive, indicates the maximal total "
                                    "time in seconds to run the fuzzer.")
+FUZZER_FLAG_INT(stale_corpus_timeout, 0,
+                "If positive, indicates the maximum time in seconds to run the "
+                "fuzzer after new input is added to the corpus.")
 FUZZER_FLAG_INT(help, 0, "Print help.")
 FUZZER_FLAG_INT(fork, 0, "Experimental mode where fuzzing happens "
                 "in a subprocess.")
diff --git a/compiler-rt/lib/fuzzer/FuzzerFork.cpp b/compiler-rt/lib/fuzzer/FuzzerFork.cpp
index e544cd846e4db..b8d3639dfcfa5 100644
--- a/compiler-rt/lib/fuzzer/FuzzerFork.cpp
+++ b/compiler-rt/lib/fuzzer/FuzzerFork.cpp
@@ -98,6 +98,7 @@ struct GlobalEnv {
   std::vector<std::size_t> FilesSizes;
   Random *Rand;
   std::chrono::system_clock::time_point ProcessStartTime;
+  std::chrono::system_clock::time_point LastNewCorpusTime;
   int Verbosity = 0;
   int Group = 0;
   int NumCorpuses = 8;
@@ -117,11 +118,18 @@ struct GlobalEnv {
         .count();
   }
 
+  size_t secondsSinceLastNewCorpus() const {
+    return std::chrono::duration_cast<std::chrono::seconds>(
+               std::chrono::system_clock::now() - LastNewCorpusTime)
+        .count();
+  }
+
   FuzzJob *CreateNewJob(size_t JobId) {
     Command Cmd(Args);
     Cmd.removeFlag("fork");
     Cmd.removeFlag("runs");
     Cmd.removeFlag("collect_data_flow");
+    Cmd.removeFlag("stale_corpus_timeout");
     for (auto &C : CorpusDirs) // Remove all corpora from the args.
       Cmd.removeArgument(C);
     Cmd.addFlag("reload", "0");  // working in an isolated dir, no reload.
@@ -196,6 +204,15 @@ struct GlobalEnv {
     return Job;
   }
 
+  void PrintStats(Stats *Stats, FuzzJob *Job) {
+    Printf("#%zd: cov: %zd ft: %zd corp: %zd exec/s: %zd oom/timeout/crash: "
+           "%zd/%zd/%zd time: %zds job: %zd dft_time: %d stale: %zd\n",
+           NumRuns, Cov.size(), Features.size(), Files.size(),
+           Stats->average_exec_per_sec, NumOOMs, NumTimeouts, NumCrashes,
+           secondsSinceProcessStartUp(), Job->JobId, Job->DftTimeInSeconds,
+           secondsSinceLastNewCorpus());
+  }
+
   void RunOneMergeJob(FuzzJob *Job) {
     auto Stats = ParseFinalStatsFromLog(Job->LogPath);
     NumRuns += Stats.number_of_executed_units;
@@ -219,14 +236,11 @@ struct GlobalEnv {
         }
       }
     }
-    // if (!FilesToAdd.empty() || Job->ExitCode != 0)
-    Printf("#%zd: cov: %zd ft: %zd corp: %zd exec/s: %zd "
-           "oom/timeout/crash: %zd/%zd/%zd time: %zds job: %zd dft_time: %d\n",
-           NumRuns, Cov.size(), Features.size(), Files.size(),
-           Stats.average_exec_per_sec, NumOOMs, NumTimeouts, NumCrashes,
-           secondsSinceProcessStartUp(), Job->JobId, Job->DftTimeInSeconds);
 
-    if (MergeCandidates.empty()) return;
+    if (MergeCandidates.empty()) {
+      PrintStats(&Stats, Job);
+      return;
+    }
 
     std::vector<std::string> FilesToAdd;
     std::set<uint32_t> NewFeatures, NewCov;
@@ -257,6 +271,9 @@ struct GlobalEnv {
         if (TPC.PcIsFuncEntry(TE))
           PrintPC("  NEW_FUNC: %p %F %L\n", "",
                   TPC.GetNextInstructionPc(TE->PC));
+    if (!FilesToAdd.empty())
+      LastNewCorpusTime = std::chrono::system_clock::now();
+    PrintStats(&Stats, Job);
   }
 
   void CollectDFT(const std::string &InputPath) {
@@ -381,6 +398,8 @@ void FuzzWithFork(Random &Rand, const FuzzingOptions &Options,
     FuzzQ.Push(Env.CreateNewJob(JobId++));
   }
 
+  Env.LastNewCorpusTime = std::chrono::system_clock::now();
+
   while (true) {
     std::unique_ptr<FuzzJob> Job(MergeQ.Pop());
     if (!Job)
@@ -462,12 +481,20 @@ void FuzzWithFork(Random &Rand, const FuzzingOptions &Options,
     // and we will wait while joining them.
     // We also don't stop instantly: other jobs need to finish.
     if (Options.MaxTotalTimeSec > 0 &&
-        Env.secondsSinceProcessStartUp() >= (size_t)Options.MaxTotalTimeSec) {
+        Env.secondsSinceProcessStartUp() > (size_t)Options.MaxTotalTimeSec) {
       Printf("INFO: fuzzed for %zd seconds, wrapping up soon\n",
              Env.secondsSinceProcessStartUp());
       StopJobs();
       break;
     }
+    if (Options.StaleCorpusTimeoutSec > 0 &&
+        Env.secondsSinceLastNewCorpus() >
+            (size_t)Options.StaleCorpusTimeoutSec) {
+      Printf("INFO: no new corpus for %zd seconds, wrapping up soon\n",
+             Env.secondsSinceLastNewCorpus());
+      StopJobs();
+      break;
+    }
     if (Env.NumRuns >= Options.MaxNumberOfRuns) {
       Printf("INFO: fuzzed for %zd iterations, wrapping up soon\n",
              Env.NumRuns);
diff --git a/compiler-rt/lib/fuzzer/FuzzerInternal.h b/compiler-rt/lib/fuzzer/FuzzerInternal.h
index 88504705137a8..e68cdf3e6daa5 100644
--- a/compiler-rt/lib/fuzzer/FuzzerInternal.h
+++ b/compiler-rt/lib/fuzzer/FuzzerInternal.h
@@ -44,10 +44,18 @@ class Fuzzer final {
         .count();
   }
 
+  size_t secondsSinceLastNewCorpus() {
+    return duration_cast<seconds>(system_clock::now() - LastNewCorpusTime)
+        .count();
+  }
+
   bool TimedOut() {
-    return Options.MaxTotalTimeSec > 0 &&
-           secondsSinceProcessStartUp() >
-               static_cast<size_t>(Options.MaxTotalTimeSec);
+    return (Options.MaxTotalTimeSec > 0 &&
+            secondsSinceProcessStartUp() >
+                static_cast<size_t>(Options.MaxTotalTimeSec)) ||
+           (Options.StaleCorpusTimeoutSec > 0 &&
+            secondsSinceLastNewCorpus() >
+                static_cast<size_t>(Options.StaleCorpusTimeoutSec));
   }
 
   size_t execPerSec() {
@@ -137,6 +145,7 @@ class Fuzzer final {
   DataFlowTrace DFT;
 
   system_clock::time_point ProcessStartTime = system_clock::now();
+  system_clock::time_point LastNewCorpusTime = system_clock::now();
   system_clock::time_point UnitStartTime, UnitStopTime;
   long TimeOfLongestUnitInSeconds = 0;
   long EpochOfLastReadOfOutputCorpus = 0;
diff --git a/compiler-rt/lib/fuzzer/FuzzerLoop.cpp b/compiler-rt/lib/fuzzer/FuzzerLoop.cpp
index 5511dfa8cf268..d872df589757d 100644
--- a/compiler-rt/lib/fuzzer/FuzzerLoop.cpp
+++ b/compiler-rt/lib/fuzzer/FuzzerLoop.cpp
@@ -352,6 +352,8 @@ void Fuzzer::PrintStats(const char *Where, const char *End, size_t Units,
 
   Printf(" exec/s: %zd", ExecPerSec);
   Printf(" rss: %zdMB", GetPeakRSSMb());
+  if (Options.StaleCorpusTimeoutSec > 0)
+    Printf(" stale: %zd", secondsSinceLastNewCorpus());
   Printf("%s", End);
 }
 
@@ -535,6 +537,7 @@ bool Fuzzer::RunOne(const uint8_t *Data, size_t Size, bool MayDeleteFile,
   size_t NumNewFeatures = Corpus.NumFeatureUpdates() - NumUpdatesBefore;
   if (NumNewFeatures || ForceAddToCorpus) {
     TPC.UpdateObservedPCs();
+    LastNewCorpusTime = UnitStopTime;
     auto NewII =
         Corpus.AddToCorpus({Data, Data + Size}, NumNewFeatures, MayDeleteFile,
                            TPC.ObservedFocusFunction(), ForceAddToCorpus,
diff --git a/compiler-rt/lib/fuzzer/FuzzerOptions.h b/compiler-rt/lib/fuzzer/FuzzerOptions.h
index 6478b63ad6935..e54e0c711facf 100644
--- a/compiler-rt/lib/fuzzer/FuzzerOptions.h
+++ b/compiler-rt/lib/fuzzer/FuzzerOptions.h
@@ -28,6 +28,7 @@ struct FuzzingOptions {
   bool IgnoreOOMs = true;
   bool IgnoreCrashes = false;
   int MaxTotalTimeSec = 0;
+  int StaleCorpusTimeoutSec = 0;
   int RssLimitMb = 0;
   int MallocLimitMb = 0;
   bool DoCrossOver = true;
diff --git a/compiler-rt/test/fuzzer/stale_corpus_timeout-reload.test b/compiler-rt/test/fuzzer/stale_corpus_timeout-reload.test
new file mode 100644
index 0000000000000..6e205f790a176
--- /dev/null
+++ b/compiler-rt/test/fuzzer/stale_corpus_timeout-reload.test
@@ -0,0 +1,15 @@
+RUN: %cpp_compiler %S/CleanseTest.cpp -o %t-CleanseTest
+
+# total time - double digits, without new corpus - one digit
+STALE_CORPUS_TIMEOUT: Done {{.*}} runs in {{[0-9][0-9]}} second(s), {{[0-9]}} second(s) without new corpus
+
+# is timer updated with a new input?
+RUN: rm -rf %t-1
+RUN: mkdir -p %t-1
+RUN: %python -c "import subprocess, time, sys, os; p = subprocess.Popen([sys.argv[1], '-seed=1', '-stale_corpus_timeout=7', '-mutate_depth=0', sys.argv[2]], stdout=subprocess.PIPE, stderr=subprocess.PIPE); time.sleep(4); open(os.path.join(sys.argv[2], 'new'), 'w').write(' 1   5    A         '); _, stderr = p.communicate(); sys.stdout.buffer.write(stderr)" %t-CleanseTest %t-1 | FileCheck %s --check-prefix=STALE_CORPUS_TIMEOUT
+
+
+# max_total_time may be triggered first
+RUN: rm -rf %t-2
+RUN: mkdir -p %t-2
+RUN: %python -c "import subprocess, time, sys, os; p = subprocess.Popen([sys.argv[1], '-seed=1', '-stale_corpus_timeout=10', '-max_total_time=10', '-mutate_depth=0', sys.argv[2]], stdout=subprocess.PIPE, stderr=subprocess.PIPE); time.sleep(4); open(os.path.join(sys.argv[2], 'new'), 'w').write(' 1   5    A         '); _, stderr = p.communicate(); sys.stdout.buffer.write(stderr)" %t-CleanseTest %t-2 | FileCheck %s --check-prefix=STALE_CORPUS_TIMEOUT
diff --git a/compiler-rt/test/fuzzer/stale_corpus_timeout.test b/compiler-rt/test/fuzzer/stale_corpus_timeout.test
new file mode 100644
index 0000000000000..9051faecf7f6d
--- /dev/null
+++ b/compiler-rt/test/fuzzer/stale_corpus_timeout.test
@@ -0,0 +1,15 @@
+RUN: %cpp_compiler %S/SingleMemcmpTest.cpp -o %t-SingleMemcmpTest
+
+STALE_CORPUS_TIMEOUT-NOT: Done 100000000 runs
+STALE_CORPUS_TIMEOUT: Done {{[0-9]+}} runs in {{[0-9]}} second(s), {{[0-9]}} second(s) without new corpus
+
+# does it work?
+RUN: %run %t-SingleMemcmpTest -seed=1 -runs=100000000 -stale_corpus_timeout=2 -use_cmp=0 2>&1 | FileCheck %s --check-prefix=STALE_CORPUS_TIMEOUT
+
+# same for -fork
+
+STALE_CORPUS_TIMEOUT_FORK-NOT: INFO: fuzzed for {{[0-9]+}} iterations
+STALE_CORPUS_TIMEOUT_FORK: INFO: no new corpus for {{[0-9]+}} seconds
+
+# does it work?
+RUN: %run %t-SingleMemcmpTest -runs=100000000 -stale_corpus_timeout=2 -use_cmp=0 -fork=1 2>&1 | FileCheck %s --check-prefix=STALE_CORPUS_TIMEOUT_FORK
diff --git a/llvm/docs/LibFuzzer.md b/llvm/docs/LibFuzzer.md
index cb2575e4b5514..914659fb548b3 100644
--- a/llvm/docs/LibFuzzer.md
+++ b/llvm/docs/LibFuzzer.md
@@ -286,6 +286,10 @@ The most important command line options are:
 : If positive, indicates the maximum total time in seconds to run the fuzzer.
   If 0 (the default), run indefinitely.
 
+`-stale_corpus_timeout`
+: If positive, indicates the maximum time in seconds to run the fuzzer after new
+  input is added to the corpus.
+
 `-merge`
 : If set to 1, any corpus inputs from the 2nd, 3rd etc. corpus directories
   that trigger new code coverage will be merged into the first corpus



More information about the llvm-commits mailing list