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