[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
Tue Jan 20 04:26:25 PST 2026


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

>From 856d2ee70cf2c9aaf39a1fb4a4a8a76112b0679f Mon Sep 17 00:00:00 2001
From: TERESH1 <svyatoslavtereshin at yandex.ru>
Date: Thu, 15 Jan 2026 14:43:11 +0000
Subject: [PATCH] [libFuzzer] Stop fuzzing if no new paths were found within a
 specified period

---
 compiler-rt/lib/fuzzer/FuzzerDriver.cpp       |  6 ++-
 compiler-rt/lib/fuzzer/FuzzerFlags.def        |  3 ++
 compiler-rt/lib/fuzzer/FuzzerFork.cpp         | 41 +++++++++++++++----
 compiler-rt/lib/fuzzer/FuzzerInternal.h       | 15 +++++--
 compiler-rt/lib/fuzzer/FuzzerLoop.cpp         |  3 ++
 compiler-rt/lib/fuzzer/FuzzerOptions.h        |  1 +
 .../test/fuzzer/exit_on_time-reload-fork.test | 26 ++++++++++++
 .../test/fuzzer/exit_on_time-reload.test      | 22 ++++++++++
 compiler-rt/test/fuzzer/exit_on_time.test     | 15 +++++++
 llvm/docs/LibFuzzer.rst                       |  3 ++
 10 files changed, 122 insertions(+), 13 deletions(-)
 create mode 100644 compiler-rt/test/fuzzer/exit_on_time-reload-fork.test
 create mode 100644 compiler-rt/test/fuzzer/exit_on_time-reload.test
 create mode 100644 compiler-rt/test/fuzzer/exit_on_time.test

diff --git a/compiler-rt/lib/fuzzer/FuzzerDriver.cpp b/compiler-rt/lib/fuzzer/FuzzerDriver.cpp
index 5928d1d96acd1..25290cd9540a4 100644
--- a/compiler-rt/lib/fuzzer/FuzzerDriver.cpp
+++ b/compiler-rt/lib/fuzzer/FuzzerDriver.cpp
@@ -697,6 +697,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.ExitOnTimeSec = Flags.exit_on_time;
   Options.DoCrossOver = Flags.cross_over;
   Options.CrossOverUniformDist = Flags.cross_over_uniform_dist;
   Options.MutateDepth = Flags.mutate_depth;
@@ -923,8 +924,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 96282b8f72a4f..f6e600d9cb543 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(exit_on_time, 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..11972de656819 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("exit_on_time");
     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 wo_finds: %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(), secondsSinceLastNewCorpus(),
+           Files.size(), Stats->average_exec_per_sec, NumOOMs, NumTimeouts,
+           NumCrashes, secondsSinceProcessStartUp(), Job->JobId,
+           Job->DftTimeInSeconds);
+  }
+
   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;
@@ -235,6 +249,9 @@ struct GlobalEnv {
     CrashResistantMerge(Args, {}, MergeCandidates, &FilesToAdd, Features,
                         &NewFeatures, Cov, &NewCov, Job->CFPath, false,
                         IsSetCoverMerge);
+    if (!FilesToAdd.empty())
+      LastNewCorpusTime = std::chrono::system_clock::now();
+    PrintStats(&Stats, Job);
     for (auto &Path : FilesToAdd) {
       auto U = FileToVector(Path);
       auto NewPath = DirPlusFile(MainCorpusDir, Hash(U));
@@ -319,7 +336,8 @@ void FuzzWithFork(Random &Rand, const FuzzingOptions &Options,
   Env.CorpusDirs = CorpusDirs;
   Env.Rand = &Rand;
   Env.Verbosity = Options.Verbosity;
-  Env.ProcessStartTime = std::chrono::system_clock::now();
+  Env.ProcessStartTime = Env.LastNewCorpusTime =
+      std::chrono::system_clock::now();
   Env.DataFlowBinary = Options.CollectDataFlow;
   Env.Group = Options.ForkCorpusGroups;
 
@@ -468,6 +486,13 @@ void FuzzWithFork(Random &Rand, const FuzzingOptions &Options,
       StopJobs();
       break;
     }
+    if (Options.ExitOnTimeSec > 0 &&
+        Env.secondsSinceLastNewCorpus() >= (size_t)Options.ExitOnTimeSec) {
+      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..f3b13a7c3fc92 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.ExitOnTimeSec > 0 &&
+            secondsSinceLastNewCorpus() >
+                static_cast<size_t>(Options.ExitOnTimeSec));
   }
 
   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 a93cd16b87934..b8c474519fc26 100644
--- a/compiler-rt/lib/fuzzer/FuzzerLoop.cpp
+++ b/compiler-rt/lib/fuzzer/FuzzerLoop.cpp
@@ -332,6 +332,8 @@ void Fuzzer::PrintStats(const char *Where, const char *End, size_t Units,
     Printf(" cov: %zd", N);
   if (size_t N = Features ? Features : Corpus.NumFeatures())
     Printf(" ft: %zd", N);
+  if (Options.ExitOnTimeSec > 0)
+    Printf(" wo_finds: %zd", secondsSinceLastNewCorpus());
   if (!Corpus.empty()) {
     Printf(" corp: %zd", Corpus.NumActiveUnits());
     if (size_t N = Corpus.SizeInBytes()) {
@@ -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..14f65f58baadf 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 ExitOnTimeSec = 0;
   int RssLimitMb = 0;
   int MallocLimitMb = 0;
   bool DoCrossOver = true;
diff --git a/compiler-rt/test/fuzzer/exit_on_time-reload-fork.test b/compiler-rt/test/fuzzer/exit_on_time-reload-fork.test
new file mode 100644
index 0000000000000..3c942876529db
--- /dev/null
+++ b/compiler-rt/test/fuzzer/exit_on_time-reload-fork.test
@@ -0,0 +1,26 @@
+# FIXME: -fork currently doesn't reload the main corpus with -reload=1
+XFAIL: *
+
+RUN: %cpp_compiler %S/CleanseTest.cpp -o %t-CleanseTest
+
+# total time - double digits
+EXIT_ON_TIME_FORK: INFO: no new corpus for {{[0-9]}} seconds, wrapping up soon
+EXIT_ON_TIME_FORK-NEXT: INFO: exiting: 0 time: {{[0-9][0-9]}}s
+
+# is timer updated with a new input?
+RUN: rm -rf %t-1
+RUN: mkdir -p %t-1
+RUN: setsid %run %t-CleanseTest -seed=1 -exit_on_time=7 -mutate_depth=0 -fork=1 %t-1 2>%t-1-log & export PID=$!
+RUN: sleep 3
+RUN: echo -n ' 1   5    A         ' > %t-1/new
+RUN: wait $PID
+RUN: cat %t-1-log | FileCheck %s --check-prefix=EXIT_ON_TIME_FORK
+
+# max_total_time may be triggered first
+RUN: rm -rf %t-2
+RUN: mkdir -p %t-2
+RUN: setsid %run %t-CleanseTest -seed=1 -exit_on_time=10 -max_total_time=10 -mutate_depth=0 %t-2 2>%t-2-log & export PID=$!
+RUN: sleep 4
+RUN: echo -n ' 1   5    A         ' > %t-2/new
+RUN: wait $PID
+RUN: cat %t-2-log | FileCheck %s --check-prefix=EXIT_ON_TIME_FORK
diff --git a/compiler-rt/test/fuzzer/exit_on_time-reload.test b/compiler-rt/test/fuzzer/exit_on_time-reload.test
new file mode 100644
index 0000000000000..2734c65fdccf6
--- /dev/null
+++ b/compiler-rt/test/fuzzer/exit_on_time-reload.test
@@ -0,0 +1,22 @@
+RUN: %cpp_compiler %S/CleanseTest.cpp -o %t-CleanseTest
+
+# total time - double digits, without new corpus - one digit
+EXIT_ON_TIME: Done {{.*}} runs in {{[0-9][0-9]}} second(s), {{[0-9]}} second(s)
+
+# is timer updated with a new input?
+RUN: rm -rf %t-1
+RUN: mkdir -p %t-1
+RUN: setsid %run %t-CleanseTest -seed=1 -exit_on_time=7 -mutate_depth=0 %t-1 2>%t-1-log & export PID=$!
+RUN: sleep 4
+RUN: echo -n ' 1   5    A         ' > %t-1/new
+RUN: wait $PID
+RUN: cat %t-1-log | FileCheck %s --check-prefix=EXIT_ON_TIME
+
+# max_total_time may be triggered first
+RUN: rm -rf %t-2
+RUN: mkdir -p %t-2
+RUN: setsid %run %t-CleanseTest -seed=1 -exit_on_time=10 -max_total_time=10 -mutate_depth=0 %t-2 2>%t-2-log & export PID=$!
+RUN: sleep 4
+RUN: echo -n ' 1   5    A         ' > %t-2/new
+RUN: wait $PID
+RUN: cat %t-2-log | FileCheck %s --check-prefix=EXIT_ON_TIME
diff --git a/compiler-rt/test/fuzzer/exit_on_time.test b/compiler-rt/test/fuzzer/exit_on_time.test
new file mode 100644
index 0000000000000..61c79a5bc22b0
--- /dev/null
+++ b/compiler-rt/test/fuzzer/exit_on_time.test
@@ -0,0 +1,15 @@
+RUN: %cpp_compiler %S/SingleMemcmpTest.cpp -o %t-SingleMemcmpTest
+
+EXIT_ON_TIME-NOT: Done 100000000 runs
+EXIT_ON_TIME: 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 -exit_on_time=2 -use_cmp=0 2>&1 | FileCheck %s --check-prefix=EXIT_ON_TIME
+
+# same for -fork
+
+EXIT_ON_TIME_FORK-NOT: INFO: fuzzed for {{[0-9]+}} iterations
+EXIT_ON_TIME_FORK: INFO: no new corpus for {{[0-9]+}} seconds
+
+# does it work?
+RUN: %run %t-SingleMemcmpTest -runs=100000000 -exit_on_time=2 -use_cmp=0 -fork=1 2>&1 | FileCheck %s --check-prefix=EXIT_ON_TIME_FORK
diff --git a/llvm/docs/LibFuzzer.rst b/llvm/docs/LibFuzzer.rst
index 2137740896ddb..3659bdf8b7d77 100644
--- a/llvm/docs/LibFuzzer.rst
+++ b/llvm/docs/LibFuzzer.rst
@@ -287,6 +287,9 @@ The most important command line options are:
 ``-max_total_time``
   If positive, indicates the maximum total time in seconds to run the fuzzer.
   If 0 (the default), run indefinitely.
+``-exit_on_time``
+  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