diff --git a/lib/Fuzzer/FuzzerDriver.cpp b/lib/Fuzzer/FuzzerDriver.cpp index 57532b4da0a..b0dc8f889d3 100644 --- a/lib/Fuzzer/FuzzerDriver.cpp +++ b/lib/Fuzzer/FuzzerDriver.cpp @@ -294,6 +294,7 @@ static int FuzzerDriver(const std::vector &Args, Options.Reload = Flags.reload; Options.OnlyASCII = Flags.only_ascii; Options.OutputCSV = Flags.output_csv; + Options.DetectLeaks = Flags.detect_leaks; if (Flags.runs >= 0) Options.MaxNumberOfRuns = Flags.runs; if (!Inputs->empty()) diff --git a/lib/Fuzzer/FuzzerFlags.def b/lib/Fuzzer/FuzzerFlags.def index a9008fac6a6..261623277a4 100644 --- a/lib/Fuzzer/FuzzerFlags.def +++ b/lib/Fuzzer/FuzzerFlags.def @@ -79,6 +79,8 @@ FUZZER_FLAG_INT(handle_term, 1, "If 1, try to intercept SIGTERM.") FUZZER_FLAG_INT(close_fd_mask, 0, "If 1, close stdout at startup; " "if 2, close stderr; if 3, close both. " "Be careful, this will also close e.g. asan's stderr/stdout.") +FUZZER_FLAG_INT(detect_leaks, 0, "If 1, and if LeakSanitizer is enabled " + "try to detect memory leaks during fuzzing (i.e. not only at shut down).") FUZZER_DEPRECATED_FLAG(exit_on_first) FUZZER_DEPRECATED_FLAG(save_minimized_corpus) diff --git a/lib/Fuzzer/FuzzerInternal.h b/lib/Fuzzer/FuzzerInternal.h index 50402f8702f..c5443b07a33 100644 --- a/lib/Fuzzer/FuzzerInternal.h +++ b/lib/Fuzzer/FuzzerInternal.h @@ -304,6 +304,7 @@ public: bool OutputCSV = false; bool PrintNewCovPcs = false; bool PrintFinalStats = false; + bool DetectLeaks = false; }; Fuzzer(UserCallback CB, MutationDispatcher &MD, FuzzingOptions Options); void AddToCorpus(const Unit &U) { @@ -366,6 +367,8 @@ private: void PrintStats(const char *Where, const char *End = "\n"); void PrintStatusForNewUnit(const Unit &U); void ShuffleCorpus(UnitVector *V); + void TryDetectingAMemoryLeak(uint8_t *Data, size_t Size); + void CheckForMemoryLeaks(); // Updates the probability distribution for the units in the corpus. // Must be called whenever the corpus or unit weights are changed. @@ -398,6 +401,8 @@ private: size_t TotalNumberOfExecutedTraceBasedMutations = 0; size_t NumberOfNewUnitsAdded = 0; + bool HasMoreMallocsThanFrees = false; + std::vector Corpus; std::unordered_set UnitHashesAddedToCorpus; diff --git a/lib/Fuzzer/FuzzerLoop.cpp b/lib/Fuzzer/FuzzerLoop.cpp index 3b00e47d233..5fe57bbb53d 100644 --- a/lib/Fuzzer/FuzzerLoop.cpp +++ b/lib/Fuzzer/FuzzerLoop.cpp @@ -18,6 +18,9 @@ #if __has_include() #include #endif +#if __has_include() +#include +#endif #endif #define NO_SANITIZE_MEMORY @@ -47,6 +50,11 @@ __sanitizer_get_coverage_pc_buffer(uintptr_t **data); __attribute__((weak)) size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size, size_t MaxSize, unsigned int Seed); +__attribute__((weak)) void __sanitizer_malloc_hook(void *ptr, size_t size); +__attribute__((weak)) void __sanitizer_free_hook(void *ptr); +__attribute__((weak)) void __lsan_enable(); +__attribute__((weak)) void __lsan_disable(); +__attribute__((weak)) int __lsan_do_recoverable_leak_check(); } namespace fuzzer { @@ -277,6 +285,7 @@ void Fuzzer::ShuffleAndMinimize() { for (auto &X : Corpus) UnitHashesAddedToCorpus.insert(Hash(X)); PrintStats("INITED"); + CheckForMemoryLeaks(); } bool Fuzzer::RunOne(const uint8_t *Data, size_t Size) { @@ -310,6 +319,26 @@ void Fuzzer::RunOneAndUpdateCorpus(uint8_t *Data, size_t Size) { ReportNewCoverage({Data, Data + Size}); } +// Leak detection is expensive, so we first check if there were more mallocs +// than frees (using the sanitizer malloc hooks) and only then try to call lsan. +struct MallocFreeTracer { + void Start() { + Mallocs = 0; + Frees = 0; + } + // Returns true if there were more mallocs than frees. + bool Stop() { return Mallocs > Frees; } + size_t Mallocs; + size_t Frees; +}; + +static thread_local MallocFreeTracer AllocTracer; + +extern "C" { +void __sanitizer_malloc_hook(void *ptr, size_t size) { AllocTracer.Mallocs++; } +void __sanitizer_free_hook(void *ptr) { AllocTracer.Frees++; } +} // extern "C" + void Fuzzer::ExecuteCallback(const uint8_t *Data, size_t Size) { UnitStartTime = system_clock::now(); // We copy the contents of Unit into a separate heap buffer @@ -319,10 +348,12 @@ void Fuzzer::ExecuteCallback(const uint8_t *Data, size_t Size) { AssignTaintLabels(DataCopy.get(), Size); CurrentUnitData = DataCopy.get(); CurrentUnitSize = Size; + AllocTracer.Start(); int Res = CB(DataCopy.get(), Size); + (void)Res; + HasMoreMallocsThanFrees = AllocTracer.Stop(); CurrentUnitSize = 0; CurrentUnitData = nullptr; - (void)Res; assert(Res == 0); } @@ -498,6 +529,47 @@ void Fuzzer::Merge(const std::vector &Corpora) { Printf("=== Merge: written %zd units\n", Res.size()); } +// Tries to call lsan, and if there are leaks exits. We call this right after +// the initial corpus was read because if there are leaky inputs in the corpus +// further fuzzing will likely hit OOMs. +void Fuzzer::CheckForMemoryLeaks() { + if (!Options.DetectLeaks) return; + if (!__lsan_do_recoverable_leak_check) + return; + if (__lsan_do_recoverable_leak_check()) { + Printf("==%d== ERROR: libFuzzer: initial corpus triggers memory leaks.\n" + "Exiting now. Use -detect_leaks=0 to disable leak detection here.\n" + "LeakSanitizer will still check for leaks at the process exit.\n", + GetPid()); + PrintFinalStats(); + _Exit(Options.ErrorExitCode); + } +} + +// Tries detecting a memory leak on the particular input that we have just +// executed before calling this function. +void Fuzzer::TryDetectingAMemoryLeak(uint8_t *Data, size_t Size) { + if (!HasMoreMallocsThanFrees) return; // mallocs==frees, a leak is unlikely. + if (!Options.DetectLeaks) return; + if (!&__lsan_enable || !&__lsan_disable || !__lsan_do_recoverable_leak_check) + return; // No lsan. + // Run the target once again, but with lsan disabled so that if there is + // a real leak we do not report it twice. + __lsan_disable(); + RunOneAndUpdateCorpus(Data, Size); + __lsan_enable(); + if (!HasMoreMallocsThanFrees) return; // a leak is unlikely. + // Now perform the actual lsan pass. This is expensive and we must ensure + // we don't call it too often. + if (__lsan_do_recoverable_leak_check()) { // Leak is found, report it. + CurrentUnitData = Data; + CurrentUnitSize = Size; + DumpCurrentUnit("leak-"); + PrintFinalStats(); + _Exit(Options.ErrorExitCode); // not exit() to disable lsan further on. + } +} + void Fuzzer::MutateAndTestOne() { MD.StartMutationSequence(); @@ -522,6 +594,7 @@ void Fuzzer::MutateAndTestOne() { StartTraceRecording(); RunOneAndUpdateCorpus(MutateInPlaceHere.data(), Size); StopTraceRecording(); + TryDetectingAMemoryLeak(MutateInPlaceHere.data(), Size); } } diff --git a/lib/Fuzzer/test/LeakTest.cpp b/lib/Fuzzer/test/LeakTest.cpp index 69ff10b2223..22e5164050e 100644 --- a/lib/Fuzzer/test/LeakTest.cpp +++ b/lib/Fuzzer/test/LeakTest.cpp @@ -8,7 +8,10 @@ static volatile void *Sink; extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { - Sink = new int; + if (Size > 0 && *Data == 'H') { + Sink = new int; + Sink = nullptr; + } return 0; } diff --git a/lib/Fuzzer/test/fuzzer-leak.test b/lib/Fuzzer/test/fuzzer-leak.test index 3690068371f..25c1f45978c 100644 --- a/lib/Fuzzer/test/fuzzer-leak.test +++ b/lib/Fuzzer/test/fuzzer-leak.test @@ -1,6 +1,20 @@ -RUN: not LLVMFuzzer-LeakTest -runs=10 2>&1 | FileCheck %s --check-prefix=LEAK -LEAK: ERROR: LeakSanitizer: detected memory leaks -LEAK-NOT: DEATH: +RUN: not LLVMFuzzer-LeakTest -runs=100000 -detect_leaks=1 2>&1 | FileCheck %s --check-prefix=LEAK_DURING +LEAK_DURING: ERROR: LeakSanitizer: detected memory leaks +LEAK_DURING: Direct leak of 4 byte(s) in 1 object(s) allocated from: +LEAK_DURING-NOT: DONE +LEAK_DURING-NOT: Done +LEAK_DURING-NOT: DEATH: + +RUN: not LLVMFuzzer-LeakTest -runs=0 -detect_leaks=1 %S 2>&1 | FileCheck %s --check-prefix=LEAK_IN_CORPUS +LEAK_IN_CORPUS: ERROR: LeakSanitizer: detected memory leaks +LEAK_IN_CORPUS: ERROR: libFuzzer: initial corpus triggers memory leaks. + + +RUN: not LLVMFuzzer-LeakTest -runs=100000 -detect_leaks=0 2>&1 | FileCheck %s --check-prefix=LEAK_AFTER +RUN: not LLVMFuzzer-LeakTest -runs=100000 2>&1 | FileCheck %s --check-prefix=LEAK_AFTER +LEAK_AFTER: Done 100000 runs in +LEAK_AFTER: ERROR: LeakSanitizer: detected memory leaks +LEAK_AFTER-NOT: DEATH: RUN: not LLVMFuzzer-LeakTimeoutTest -timeout=1 2>&1 | FileCheck %s --check-prefix=LEAK_TIMEOUT LEAK_TIMEOUT: ERROR: libFuzzer: timeout after