1 //===- FuzzerFork.cpp - run fuzzing in separate subprocesses --------------===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 // Spawn and orchestrate separate fuzzing processes. 9 //===----------------------------------------------------------------------===// 10 11 #include "FuzzerCommand.h" 12 #include "FuzzerFork.h" 13 #include "FuzzerIO.h" 14 #include "FuzzerInternal.h" 15 #include "FuzzerMerge.h" 16 #include "FuzzerSHA1.h" 17 #include "FuzzerTracePC.h" 18 #include "FuzzerUtil.h" 19 20 #include <atomic> 21 #include <chrono> 22 #include <fstream> 23 #include <memory> 24 #include <mutex> 25 #include <queue> 26 #include <sstream> 27 #include <thread> 28 29 namespace fuzzer { 30 31 struct Stats { 32 size_t number_of_executed_units = 0; 33 size_t peak_rss_mb = 0; 34 size_t average_exec_per_sec = 0; 35 }; 36 37 static Stats ParseFinalStatsFromLog(const std::string &LogPath) { 38 std::ifstream In(LogPath); 39 std::string Line; 40 Stats Res; 41 struct { 42 const char *Name; 43 size_t *Var; 44 } NameVarPairs[] = { 45 {"stat::number_of_executed_units:", &Res.number_of_executed_units}, 46 {"stat::peak_rss_mb:", &Res.peak_rss_mb}, 47 {"stat::average_exec_per_sec:", &Res.average_exec_per_sec}, 48 {nullptr, nullptr}, 49 }; 50 while (std::getline(In, Line, '\n')) { 51 if (Line.find("stat::") != 0) continue; 52 std::istringstream ISS(Line); 53 std::string Name; 54 size_t Val; 55 ISS >> Name >> Val; 56 for (size_t i = 0; NameVarPairs[i].Name; i++) 57 if (Name == NameVarPairs[i].Name) 58 *NameVarPairs[i].Var = Val; 59 } 60 return Res; 61 } 62 63 struct FuzzJob { 64 // Inputs. 65 Command Cmd; 66 std::string CorpusDir; 67 std::string FeaturesDir; 68 std::string LogPath; 69 std::string SeedListPath; 70 std::string CFPath; 71 72 // Fuzzing Outputs. 73 int ExitCode; 74 75 ~FuzzJob() { 76 RemoveFile(CFPath); 77 RemoveFile(LogPath); 78 RemoveFile(SeedListPath); 79 RmDirRecursive(CorpusDir); 80 RmDirRecursive(FeaturesDir); 81 } 82 }; 83 84 struct GlobalEnv { 85 Vector<std::string> Args; 86 Vector<std::string> CorpusDirs; 87 std::string MainCorpusDir; 88 std::string TempDir; 89 Set<uint32_t> Features, Cov; 90 Vector<std::string> Files; 91 Random *Rand; 92 std::chrono::system_clock::time_point ProcessStartTime; 93 int Verbosity = 0; 94 95 size_t NumTimeouts = 0; 96 size_t NumOOMs = 0; 97 size_t NumCrashes = 0; 98 99 100 size_t NumRuns = 0; 101 102 size_t secondsSinceProcessStartUp() const { 103 return std::chrono::duration_cast<std::chrono::seconds>( 104 std::chrono::system_clock::now() - ProcessStartTime) 105 .count(); 106 } 107 108 FuzzJob *CreateNewJob(size_t JobId) { 109 Command Cmd(Args); 110 Cmd.removeFlag("fork"); 111 Cmd.removeFlag("runs"); 112 for (auto &C : CorpusDirs) // Remove all corpora from the args. 113 Cmd.removeArgument(C); 114 Cmd.addFlag("reload", "0"); // working in an isolated dir, no reload. 115 Cmd.addFlag("print_final_stats", "1"); 116 Cmd.addFlag("print_funcs", "0"); // no need to spend time symbolizing. 117 Cmd.addFlag("max_total_time", std::to_string(std::min((size_t)300, JobId))); 118 119 auto Job = new FuzzJob; 120 std::string Seeds; 121 if (size_t CorpusSubsetSize = 122 std::min(Files.size(), (size_t)sqrt(Files.size() + 2))) 123 for (size_t i = 0; i < CorpusSubsetSize; i++) 124 Seeds += (Seeds.empty() ? "" : ",") + 125 Files[Rand->SkewTowardsLast(Files.size())]; 126 if (!Seeds.empty()) { 127 Job->SeedListPath = std::to_string(JobId) + ".seeds"; 128 WriteToFile(Seeds, Job->SeedListPath); 129 Cmd.addFlag("seed_inputs", "@" + Job->SeedListPath); 130 } 131 Job->LogPath = DirPlusFile(TempDir, std::to_string(JobId) + ".log"); 132 Job->CorpusDir = DirPlusFile(TempDir, "C" + std::to_string(JobId)); 133 Job->FeaturesDir = DirPlusFile(TempDir, "F" + std::to_string(JobId)); 134 Job->CFPath = DirPlusFile(TempDir, std::to_string(JobId) + ".merge"); 135 136 137 Cmd.addArgument(Job->CorpusDir); 138 Cmd.addFlag("features_dir", Job->FeaturesDir); 139 140 for (auto &D : {Job->CorpusDir, Job->FeaturesDir}) { 141 RmDirRecursive(D); 142 MkDir(D); 143 } 144 145 Cmd.setOutputFile(Job->LogPath); 146 Cmd.combineOutAndErr(); 147 148 Job->Cmd = Cmd; 149 150 if (Verbosity >= 2) 151 Printf("Job %zd/%p Created: %s\n", JobId, Job, 152 Job->Cmd.toString().c_str()); 153 // Start from very short runs and gradually increase them. 154 return Job; 155 } 156 157 void RunOneMergeJob(FuzzJob *Job) { 158 auto Stats = ParseFinalStatsFromLog(Job->LogPath); 159 NumRuns += Stats.number_of_executed_units; 160 161 Vector<SizedFile> TempFiles, MergeCandidates; 162 // Read all newly created inputs and their feature sets. 163 // Choose only those inputs that have new features. 164 GetSizedFilesFromDir(Job->CorpusDir, &TempFiles); 165 std::sort(TempFiles.begin(), TempFiles.end()); 166 for (auto &F : TempFiles) { 167 auto FeatureFile = F.File; 168 FeatureFile.replace(0, Job->CorpusDir.size(), Job->FeaturesDir); 169 auto FeatureBytes = FileToVector(FeatureFile, 0, false); 170 assert((FeatureBytes.size() % sizeof(uint32_t)) == 0); 171 Vector<uint32_t> NewFeatures(FeatureBytes.size() / sizeof(uint32_t)); 172 memcpy(NewFeatures.data(), FeatureBytes.data(), FeatureBytes.size()); 173 for (auto Ft : NewFeatures) { 174 if (!Features.count(Ft)) { 175 MergeCandidates.push_back(F); 176 break; 177 } 178 } 179 } 180 if (MergeCandidates.empty()) return; 181 182 Vector<std::string> FilesToAdd; 183 Set<uint32_t> NewFeatures, NewCov; 184 CrashResistantMerge(Args, {}, MergeCandidates, &FilesToAdd, Features, 185 &NewFeatures, Cov, &NewCov, Job->CFPath, false); 186 for (auto &Path : FilesToAdd) { 187 auto U = FileToVector(Path); 188 auto NewPath = DirPlusFile(MainCorpusDir, Hash(U)); 189 WriteToFile(U, NewPath); 190 Files.push_back(NewPath); 191 } 192 Features.insert(NewFeatures.begin(), NewFeatures.end()); 193 Cov.insert(NewCov.begin(), NewCov.end()); 194 for (auto Idx : NewCov) 195 if (auto *TE = TPC.PCTableEntryByIdx(Idx)) 196 if (TPC.PcIsFuncEntry(TE)) 197 PrintPC(" NEW_FUNC: %p %F %L\n", "", 198 TPC.GetNextInstructionPc(TE->PC)); 199 200 if (!FilesToAdd.empty() || Job->ExitCode != 0) 201 Printf("#%zd: cov: %zd ft: %zd corp: %zd exec/s %zd " 202 "oom/timeout/crash: %zd/%zd/%zd time: %zds\n", NumRuns, 203 Cov.size(), Features.size(), Files.size(), 204 Stats.average_exec_per_sec, 205 NumOOMs, NumTimeouts, NumCrashes, secondsSinceProcessStartUp()); 206 } 207 }; 208 209 struct JobQueue { 210 std::queue<FuzzJob *> Qu; 211 std::mutex Mu; 212 213 void Push(FuzzJob *Job) { 214 std::lock_guard<std::mutex> Lock(Mu); 215 Qu.push(Job); 216 } 217 FuzzJob *Pop() { 218 std::lock_guard<std::mutex> Lock(Mu); 219 if (Qu.empty()) return nullptr; 220 auto Job = Qu.front(); 221 Qu.pop(); 222 return Job; 223 } 224 }; 225 226 void WorkerThread(std::atomic<bool> *Stop, JobQueue *FuzzQ, JobQueue *MergeQ) { 227 while (!Stop->load()) { 228 auto Job = FuzzQ->Pop(); 229 // Printf("WorkerThread: job %p\n", Job); 230 if (!Job) { 231 SleepSeconds(1); 232 continue; 233 } 234 Job->ExitCode = ExecuteCommand(Job->Cmd); 235 MergeQ->Push(Job); 236 } 237 } 238 239 // This is just a skeleton of an experimental -fork=1 feature. 240 void FuzzWithFork(Random &Rand, const FuzzingOptions &Options, 241 const Vector<std::string> &Args, 242 const Vector<std::string> &CorpusDirs, int NumJobs) { 243 Printf("INFO: -fork=%d: fuzzing in separate process(s)\n", NumJobs); 244 245 GlobalEnv Env; 246 Env.Args = Args; 247 Env.CorpusDirs = CorpusDirs; 248 Env.Rand = &Rand; 249 Env.Verbosity = Options.Verbosity; 250 Env.ProcessStartTime = std::chrono::system_clock::now(); 251 252 Vector<SizedFile> SeedFiles; 253 for (auto &Dir : CorpusDirs) 254 GetSizedFilesFromDir(Dir, &SeedFiles); 255 std::sort(SeedFiles.begin(), SeedFiles.end()); 256 Env.TempDir = TempPath(".dir"); 257 RmDirRecursive(Env.TempDir); // in case there is a leftover from old runs. 258 MkDir(Env.TempDir); 259 260 261 if (CorpusDirs.empty()) 262 MkDir(Env.MainCorpusDir = DirPlusFile(Env.TempDir, "C")); 263 else 264 Env.MainCorpusDir = CorpusDirs[0]; 265 266 auto CFPath = DirPlusFile(Env.TempDir, "merge.txt"); 267 CrashResistantMerge(Env.Args, {}, SeedFiles, &Env.Files, {}, &Env.Features, 268 {}, &Env.Cov, 269 CFPath, false); 270 RemoveFile(CFPath); 271 Printf("INFO: -fork=%d: %zd seed inputs, starting to fuzz in %s\n", NumJobs, 272 Env.Files.size(), Env.TempDir.c_str()); 273 274 int ExitCode = 0; 275 276 JobQueue FuzzQ, MergeQ; 277 std::atomic<bool> Stop(false); 278 279 size_t JobId = 1; 280 Vector<std::thread> Threads; 281 for (int t = 0; t < NumJobs; t++) { 282 Threads.push_back(std::thread(WorkerThread, &Stop, &FuzzQ, &MergeQ)); 283 FuzzQ.Push(Env.CreateNewJob(JobId++)); 284 } 285 286 while (true) { 287 std::unique_ptr<FuzzJob> Job(MergeQ.Pop()); 288 if (!Job) { 289 if (Stop) 290 break; 291 SleepSeconds(1); 292 continue; 293 } 294 ExitCode = Job->ExitCode; 295 if (ExitCode == Options.InterruptExitCode) { 296 Printf("==%lu== libFuzzer: a child was interrupted; exiting\n", GetPid()); 297 Stop = true; 298 break; 299 } 300 Fuzzer::MaybeExitGracefully(); 301 302 Env.RunOneMergeJob(Job.get()); 303 304 // Continue if our crash is one of the ignorred ones. 305 if (Options.IgnoreTimeouts && ExitCode == Options.TimeoutExitCode) 306 Env.NumTimeouts++; 307 else if (Options.IgnoreOOMs && ExitCode == Options.OOMExitCode) 308 Env.NumOOMs++; 309 else if (ExitCode != 0) { 310 Env.NumCrashes++; 311 if (Options.IgnoreCrashes) { 312 std::ifstream In(Job->LogPath); 313 std::string Line; 314 while (std::getline(In, Line, '\n')) 315 if (Line.find("ERROR:") != Line.npos || 316 Line.find("runtime error:") != Line.npos) 317 Printf("%s\n", Line.c_str()); 318 } else { 319 // And exit if we don't ignore this crash. 320 Printf("INFO: log from the inner process:\n%s", 321 FileToString(Job->LogPath).c_str()); 322 Stop = true; 323 } 324 } 325 326 // Stop if we are over the time budget. 327 // This is not precise, since other threads are still running 328 // and we will wait while joining them. 329 // We also don't stop instantly: other jobs need to finish. 330 if (Options.MaxTotalTimeSec > 0 && !Stop && 331 Env.secondsSinceProcessStartUp() >= (size_t)Options.MaxTotalTimeSec) { 332 Printf("INFO: fuzzed for %zd seconds, wrapping up soon\n", 333 Env.secondsSinceProcessStartUp()); 334 Stop = true; 335 } 336 if (!Stop && Env.NumRuns >= Options.MaxNumberOfRuns) { 337 Printf("INFO: fuzzed for %zd iterations, wrapping up soon\n", 338 Env.NumRuns); 339 Stop = true; 340 } 341 342 if (!Stop) 343 FuzzQ.Push(Env.CreateNewJob(JobId++)); 344 } 345 Stop = true; 346 347 for (auto &T : Threads) 348 T.join(); 349 350 // The workers have terminated. Don't try to remove the directory before they 351 // terminate to avoid a race condition preventing cleanup on Windows. 352 RmDirRecursive(Env.TempDir); 353 354 // Use the exit code from the last child process. 355 Printf("INFO: exiting: %d time: %zds\n", ExitCode, 356 Env.secondsSinceProcessStartUp()); 357 exit(ExitCode); 358 } 359 360 } // namespace fuzzer 361