[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