[libFuzzer] Stop fuzzing if no new corpus were found within a specified period (#176177)

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`.

GitOrigin-RevId: af2d76922b54fc347faac9727b0748387fea043f
diff --git a/FuzzerDriver.cpp b/FuzzerDriver.cpp
index fda7880..d3b8852 100644
--- a/FuzzerDriver.cpp
+++ b/FuzzerDriver.cpp
@@ -698,6 +698,7 @@
   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 @@
   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/FuzzerFlags.def b/FuzzerFlags.def
index 9b1da61..cb12419 100644
--- a/FuzzerFlags.def
+++ b/FuzzerFlags.def
@@ -56,6 +56,9 @@
   "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/FuzzerFork.cpp b/FuzzerFork.cpp
index e544cd8..b8d3639 100644
--- a/FuzzerFork.cpp
+++ b/FuzzerFork.cpp
@@ -98,6 +98,7 @@
   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 @@
         .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 @@
     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 @@
         }
       }
     }
-    // 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 @@
         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 @@
     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 @@
     // 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/FuzzerInternal.h b/FuzzerInternal.h
index 8850470..e68cdf3 100644
--- a/FuzzerInternal.h
+++ b/FuzzerInternal.h
@@ -44,10 +44,18 @@
         .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 @@
   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/FuzzerLoop.cpp b/FuzzerLoop.cpp
index 5511dfa..55f6f7b 100644
--- a/FuzzerLoop.cpp
+++ b/FuzzerLoop.cpp
@@ -352,6 +352,8 @@
 
   Printf(" exec/s: %zd", ExecPerSec);
   Printf(" rss: %zdMB", GetPeakRSSMb());
+  if (Options.StaleCorpusTimeoutSec > 0)
+    Printf(" stale: %zd", secondsSinceLastNewCorpus());
   Printf("%s", End);
 }
 
@@ -535,6 +537,7 @@
   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,
@@ -874,7 +877,8 @@
   DFT.Clear();  // No need for DFT any more.
   TPC.SetPrintNewPCs(Options.PrintNewCovPcs);
   TPC.SetPrintNewFuncs(Options.PrintNewCovFuncs);
-  system_clock::time_point LastCorpusReload = system_clock::now();
+  system_clock::time_point LastCorpusReload = LastNewCorpusTime =
+      system_clock::now();
 
   TmpMaxMutationLen =
       Min(MaxMutationLen, Max(size_t(4), Corpus.MaxInputSize()));
diff --git a/FuzzerOptions.h b/FuzzerOptions.h
index 6478b63..e54e0c7 100644
--- a/FuzzerOptions.h
+++ b/FuzzerOptions.h
@@ -28,6 +28,7 @@
   bool IgnoreOOMs = true;
   bool IgnoreCrashes = false;
   int MaxTotalTimeSec = 0;
+  int StaleCorpusTimeoutSec = 0;
   int RssLimitMb = 0;
   int MallocLimitMb = 0;
   bool DoCrossOver = true;