[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