[libFuzzer] introduce an experimental mode -fork=1, where fuzzing happens in a subprocess (still running multiple inputs per process), thus making the fuzzing more resilient to timeouts and OOMs. This is just a skeleton of the code, and some associated refactoring, not a fully working feature yet.

llvm-svn: 353570
This commit is contained in:
Kostya Serebryany 2019-02-08 21:27:23 +00:00
parent 3bf72d7d64
commit f762a11544
7 changed files with 89 additions and 52 deletions

View File

@ -16,6 +16,7 @@
#include "FuzzerMutate.h"
#include "FuzzerRandom.h"
#include "FuzzerTracePC.h"
#include "FuzzerMerge.h"
#include <algorithm>
#include <atomic>
#include <chrono>
@ -24,6 +25,7 @@
#include <mutex>
#include <string>
#include <thread>
#include <fstream>
// This function should be present in the libFuzzer so that the client
// binary can test for its existence.
@ -304,6 +306,11 @@ static std::string GetDedupTokenFromFile(const std::string &Path) {
return S.substr(Beg, End - Beg);
}
static std::string TempPath(const char *Extension) {
return DirPlusFile(TmpDir(),
"libFuzzerTemp." + std::to_string(GetPid()) + Extension);
}
int CleanseCrashInput(const Vector<std::string> &Args,
const FuzzingOptions &Options) {
if (Inputs->size() != 1 || !Flags.exact_artifact_path) {
@ -319,10 +326,8 @@ int CleanseCrashInput(const Vector<std::string> &Args,
assert(Cmd.hasArgument(InputFilePath));
Cmd.removeArgument(InputFilePath);
auto LogFilePath = DirPlusFile(
TmpDir(), "libFuzzerTemp." + std::to_string(GetPid()) + ".txt");
auto TmpFilePath = DirPlusFile(
TmpDir(), "libFuzzerTemp." + std::to_string(GetPid()) + ".repro");
auto LogFilePath = TempPath(".txt");
auto TmpFilePath = TempPath(".repro");
Cmd.addArgument(TmpFilePath);
Cmd.setOutputFile(LogFilePath);
Cmd.combineOutAndErr();
@ -382,8 +387,7 @@ int MinimizeCrashInput(const Vector<std::string> &Args,
BaseCmd.addFlag("max_total_time", "600");
}
auto LogFilePath = DirPlusFile(
TmpDir(), "libFuzzerTemp." + std::to_string(GetPid()) + ".txt");
auto LogFilePath = TempPath(".txt");
BaseCmd.setOutputFile(LogFilePath);
BaseCmd.combineOutAndErr();
@ -467,6 +471,36 @@ int MinimizeCrashInputInternalStep(Fuzzer *F, InputCorpus *Corpus) {
return 0;
}
// This is just a sceleton of an experimental -fork=1 feature.
void FuzzWithFork(const FuzzingOptions &Options,
const Vector<std::string> &Args,
const Vector<std::string> &Corpora) {
auto CFPath = TempPath(".fork");
Printf("INFO: -fork=1: doing fuzzing in a separate process in order to "
"be more resistant to crashes, timeouts, and OOMs\n");
auto Files =
CrashResistantMerge(Args, Corpora, CFPath, nullptr, nullptr);
Printf("INFO: -fork=1: seed corpus analyzed, %zd seeds chosen, starting to "
"fuzz in separate processes\n", Files.size());
Command Cmd(Args);
Cmd.removeFlag("fork");
if (Files.size() >= 2)
Cmd.addFlag("seed_inputs",
Files.back() + "," + Files[Files.size() - 2]);
Cmd.addFlag("runs", "1000000");
Cmd.addFlag("max_total_time", "30");
for (size_t i = 0; i < 1000; i++) {
Printf("RUN %s\n", Cmd.toString().c_str());
int ExitCode = ExecuteCommand(Cmd);
// TODO: sniff the crash, ignore OOMs and timeouts.
if (ExitCode != 0) break;
}
RemoveFile(CFPath);
exit(0);
}
int AnalyzeDictionary(Fuzzer *F, const Vector<Unit>& Dict,
UnitVector& Corpus) {
Printf("Started dictionary minimization (up to %d tests)\n",
@ -694,11 +728,25 @@ int FuzzerDriver(int *argc, char ***argv, UserCallback Callback) {
exit(0);
}
if (Flags.fork)
FuzzWithFork(Options, Args, *Inputs);
if (Flags.merge) {
F->CrashResistantMerge(Args, *Inputs,
Flags.load_coverage_summary,
Flags.save_coverage_summary,
Flags.merge_control_file);
if (Inputs->size() < 2) {
Printf("INFO: Merge requires two or more corpus dirs\n");
exit(0);
}
std::string CFPath =
Flags.merge_control_file ? Flags.merge_control_file : TempPath(".txt");
auto Files =
CrashResistantMerge(Args, *Inputs, CFPath, Flags.load_coverage_summary,
Flags.save_coverage_summary);
for (auto &Path : Files)
F->WriteToOutputCorpus(FileToVector(Path, Options.MaxLen));
// We are done, delete the control file if it was a temporary one.
if (!Flags.merge_control_file)
RemoveFile(CFPath);
exit(0);
}

View File

@ -41,6 +41,8 @@ FUZZER_FLAG_INT(timeout_exitcode, 77, "When libFuzzer reports a timeout "
FUZZER_FLAG_INT(max_total_time, 0, "If positive, indicates the maximal total "
"time in seconds to run the fuzzer.")
FUZZER_FLAG_INT(help, 0, "Print help.")
FUZZER_FLAG_INT(fork, 0, "Experimental mode where fuzzing happens "
"in a subprocess")
FUZZER_FLAG_INT(merge, 0, "If 1, the 2-nd, 3-rd, etc corpora will be "
"merged into the 1-st corpus. Only interesting units will be taken. "
"This flag can be used to minimize a corpus.")

View File

@ -73,11 +73,6 @@ public:
// Merge Corpora[1:] into Corpora[0].
void Merge(const Vector<std::string> &Corpora);
void CrashResistantMerge(const Vector<std::string> &Args,
const Vector<std::string> &Corpora,
const char *CoverageSummaryInputPathOrNull,
const char *CoverageSummaryOutputPathOrNull,
const char *MergeControlFilePathOrNull);
void CrashResistantMergeInternalStep(const std::string &ControlFilePath);
MutationDispatcher &GetMD() { return MD; }
void PrintFinalStats();
@ -91,19 +86,19 @@ public:
bool DuringInitialCorpusExecution);
void HandleMalloc(size_t Size);
static void MaybeExitGracefully();
void WriteToOutputCorpus(const Unit &U);
private:
void AlarmCallback();
void CrashCallback();
void ExitCallback();
void MaybeExitGracefully();
void CrashOnOverwrittenData();
void InterruptCallback();
void MutateAndTestOne();
void PurgeAllocator();
void ReportNewCoverage(InputInfo *II, const Unit &U);
void PrintPulseAndReportSlowInput(const uint8_t *Data, size_t Size);
void WriteToOutputCorpus(const Unit &U);
void WriteUnitToFileWithPrefix(const Unit &U, const char *Prefix);
void PrintStats(const char *Where, const char *End = "\n", size_t Units = 0);
void PrintStatusForNewUnit(const Unit &U, const char *Text);

View File

@ -256,9 +256,9 @@ void Fuzzer::ExitCallback() {
}
void Fuzzer::MaybeExitGracefully() {
if (!GracefulExitRequested) return;
if (!F->GracefulExitRequested) return;
Printf("==%lu== INFO: libFuzzer: exiting as requested\n", GetPid());
PrintFinalStats();
F->PrintFinalStats();
_Exit(0);
}

View File

@ -122,7 +122,7 @@ size_t Merger::ApproximateMemoryConsumption() const {
// Decides which files need to be merged (add thost to NewFiles).
// Returns the number of new features added.
size_t Merger::Merge(const Set<uint32_t> &InitialFeatures,
size_t Merger::Merge(const Set<uint32_t> &InitialFeatures,
Vector<std::string> *NewFiles) {
NewFiles->clear();
assert(NumFilesInFirstCorpus <= Files.size());
@ -223,7 +223,7 @@ void Fuzzer::CrashResistantMergeInternalStep(const std::string &CFPath) {
std::ofstream OF(CFPath, std::ofstream::out | std::ofstream::app);
Set<size_t> AllFeatures;
for (size_t i = M.FirstNotProcessedFile; i < M.Files.size(); i++) {
MaybeExitGracefully();
Fuzzer::MaybeExitGracefully();
auto U = FileToVector(M.Files[i].Name);
if (U.size() > MaxInputLen) {
U.resize(MaxInputLen);
@ -275,27 +275,18 @@ static void WriteNewControlFile(const std::string &CFPath,
}
// Outer process. Does not call the target code and thus sohuld not fail.
void Fuzzer::CrashResistantMerge(const Vector<std::string> &Args,
const Vector<std::string> &Corpora,
const char *CoverageSummaryInputPathOrNull,
const char *CoverageSummaryOutputPathOrNull,
const char *MergeControlFilePathOrNull) {
if (Corpora.size() <= 1) {
Printf("Merge requires two or more corpus dirs\n");
return;
}
auto CFPath =
MergeControlFilePathOrNull
? MergeControlFilePathOrNull
: DirPlusFile(TmpDir(),
"libFuzzerTemp." + std::to_string(GetPid()) + ".txt");
Vector<std::string>
CrashResistantMerge(const Vector<std::string> &Args,
const Vector<std::string> &Corpora,
const std::string &CFPath,
const char *CoverageSummaryInputPathOrNull,
const char *CoverageSummaryOutputPathOrNull) {
size_t NumAttempts = 0;
if (MergeControlFilePathOrNull && FileSize(MergeControlFilePathOrNull)) {
if (FileSize(CFPath)) {
Printf("MERGE-OUTER: non-empty control file provided: '%s'\n",
MergeControlFilePathOrNull);
CFPath.c_str());
Merger M;
std::ifstream IF(MergeControlFilePathOrNull);
std::ifstream IF(CFPath);
if (M.Parse(IF, /*ParseCoverage=*/false)) {
Printf("MERGE-OUTER: control file ok, %zd files total,"
" first not processed file %zd\n",
@ -334,9 +325,10 @@ void Fuzzer::CrashResistantMerge(const Vector<std::string> &Args,
// Every inner process should execute at least one input.
Command BaseCmd(Args);
BaseCmd.removeFlag("merge");
BaseCmd.removeFlag("fork");
bool Success = false;
for (size_t Attempt = 1; Attempt <= NumAttempts; Attempt++) {
MaybeExitGracefully();
Fuzzer::MaybeExitGracefully();
Printf("MERGE-OUTER: attempt %zd\n", Attempt);
Command Cmd(BaseCmd);
Cmd.addFlag("merge_control_file", CFPath);
@ -368,7 +360,6 @@ void Fuzzer::CrashResistantMerge(const Vector<std::string> &Args,
std::ofstream SummaryOut(CoverageSummaryOutputPathOrNull);
M.PrintSummary(SummaryOut);
}
Vector<std::string> NewFiles;
Set<uint32_t> InitialFeatures;
if (CoverageSummaryInputPathOrNull) {
std::ifstream SummaryIn(CoverageSummaryInputPathOrNull);
@ -376,14 +367,11 @@ void Fuzzer::CrashResistantMerge(const Vector<std::string> &Args,
Printf("MERGE-OUTER: coverage summary loaded from %s, %zd features found\n",
CoverageSummaryInputPathOrNull, InitialFeatures.size());
}
Vector<std::string> NewFiles;
size_t NumNewFeatures = M.Merge(InitialFeatures, &NewFiles);
Printf("MERGE-OUTER: %zd new files with %zd new features added\n",
NewFiles.size(), NumNewFeatures);
for (auto &F: NewFiles)
WriteToOutputCorpus(FileToVector(F, MaxInputLen));
// We are done, delete the control file if it was a temporary one.
if (!MergeControlFilePathOrNull)
RemoveFile(CFPath);
return NewFiles;
}
} // namespace fuzzer

View File

@ -67,13 +67,17 @@ struct Merger {
Set<uint32_t> ParseSummary(std::istream &IS);
size_t Merge(const Set<uint32_t> &InitialFeatures,
Vector<std::string> *NewFiles);
size_t Merge(Vector<std::string> *NewFiles) {
return Merge(Set<uint32_t>{}, NewFiles);
}
size_t ApproximateMemoryConsumption() const;
Set<uint32_t> AllFeatures() const;
};
Vector<std::string>
CrashResistantMerge(const Vector<std::string> &Args,
const Vector<std::string> &Corpora,
const std::string &CFPath,
const char *CoverageSummaryInputPathOrNull,
const char *CoverageSummaryOutputPathOrNull);
} // namespace fuzzer
#endif // LLVM_FUZZER_MERGE_H

View File

@ -647,7 +647,7 @@ static void Merge(const std::string &Input,
EXPECT_TRUE(M.Parse(Input, true));
std::stringstream SS;
M.PrintSummary(SS);
EXPECT_EQ(NumNewFeatures, M.Merge(&NewFiles));
EXPECT_EQ(NumNewFeatures, M.Merge({}, &NewFiles));
EXPECT_EQ(M.AllFeatures(), M.ParseSummary(SS));
EQ(NewFiles, Result);
}
@ -705,7 +705,7 @@ TEST(Merge, Good) {
EQ(M.Files[0].Features, {1, 2, 3});
EQ(M.Files[1].Features, {4, 5, 6});
EQ(M.Files[2].Features, {1, 3, 6});
EXPECT_EQ(0U, M.Merge(&NewFiles));
EXPECT_EQ(0U, M.Merge({}, &NewFiles));
EQ(NewFiles, {});
EXPECT_TRUE(M.Parse("3\n1\nA\nB\nC\n"
@ -716,7 +716,7 @@ TEST(Merge, Good) {
EQ(M.Files[0].Features, {1, 2, 3});
EQ(M.Files[1].Features, {4, 5, 6});
EQ(M.Files[2].Features, {1, 3, 6});
EXPECT_EQ(3U, M.Merge(&NewFiles));
EXPECT_EQ(3U, M.Merge({}, &NewFiles));
EQ(NewFiles, {"B"});
// Same as the above, but with InitialFeatures.