From 92fb310151d2b1e349695fc0f1c5d5d50afb3b52 Mon Sep 17 00:00:00 2001 From: Kostya Serebryany Date: Tue, 28 Jun 2022 11:36:30 -0700 Subject: [PATCH] [libFuzzer] Extend the fuzz target intarface to allow -1 return value. With this change, fuzz targets may choose to return -1 to indicate that the input should not be added to the corpus regardless of the coverage it generated. Reviewed By: morehouse Differential Revision: https://reviews.llvm.org/D128749 --- compiler-rt/lib/fuzzer/FuzzerInternal.h | 5 +++- compiler-rt/lib/fuzzer/FuzzerLoop.cpp | 24 ++++++++++++------- compiler-rt/test/fuzzer/Reject.cpp | 23 ++++++++++++++++++ compiler-rt/test/fuzzer/not-instrumented.test | 4 ++-- compiler-rt/test/fuzzer/reject.test | 9 +++++++ llvm/docs/LibFuzzer.rst | 24 ++++++++++++++++++- 6 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 compiler-rt/test/fuzzer/Reject.cpp create mode 100644 compiler-rt/test/fuzzer/reject.test diff --git a/compiler-rt/lib/fuzzer/FuzzerInternal.h b/compiler-rt/lib/fuzzer/FuzzerInternal.h index 6637b0034e5527..31f54eaa478a84 100644 --- a/compiler-rt/lib/fuzzer/FuzzerInternal.h +++ b/compiler-rt/lib/fuzzer/FuzzerInternal.h @@ -65,7 +65,10 @@ class Fuzzer { static void StaticFileSizeExceedCallback(); static void StaticGracefulExitCallback(); - void ExecuteCallback(const uint8_t *Data, size_t Size); + // Executes the target callback on {Data, Size} once. + // Returns false if the input was rejected by the target (target returned -1), + // and true otherwise. + bool ExecuteCallback(const uint8_t *Data, size_t Size); bool RunOne(const uint8_t *Data, size_t Size, bool MayDeleteFile = false, InputInfo *II = nullptr, bool ForceAddToCorpus = false, bool *FoundUniqFeatures = nullptr); diff --git a/compiler-rt/lib/fuzzer/FuzzerLoop.cpp b/compiler-rt/lib/fuzzer/FuzzerLoop.cpp index 3205942f6d84df..f095757229e9ec 100644 --- a/compiler-rt/lib/fuzzer/FuzzerLoop.cpp +++ b/compiler-rt/lib/fuzzer/FuzzerLoop.cpp @@ -511,7 +511,7 @@ bool Fuzzer::RunOne(const uint8_t *Data, size_t Size, bool MayDeleteFile, // Largest input length should be INT_MAX. assert(Size < std::numeric_limits::max()); - ExecuteCallback(Data, Size); + if(!ExecuteCallback(Data, Size)) return false; auto TimeOfUnit = duration_cast(UnitStopTime - UnitStartTime); UniqFeatureSetTmp.clear(); @@ -586,7 +586,7 @@ static bool LooseMemeq(const uint8_t *A, const uint8_t *B, size_t Size) { // This method is not inlined because it would cause a test to fail where it // is part of the stack unwinding. See D97975 for details. -ATTRIBUTE_NOINLINE void Fuzzer::ExecuteCallback(const uint8_t *Data, +ATTRIBUTE_NOINLINE bool Fuzzer::ExecuteCallback(const uint8_t *Data, size_t Size) { TPC.RecordInitialStack(); TotalNumberOfRuns++; @@ -602,23 +602,24 @@ ATTRIBUTE_NOINLINE void Fuzzer::ExecuteCallback(const uint8_t *Data, if (CurrentUnitData && CurrentUnitData != Data) memcpy(CurrentUnitData, Data, Size); CurrentUnitSize = Size; + int CBRes = 0; { ScopedEnableMsanInterceptorChecks S; AllocTracer.Start(Options.TraceMalloc); UnitStartTime = system_clock::now(); TPC.ResetMaps(); RunningUserCallback = true; - int Res = CB(DataCopy, Size); + CBRes = CB(DataCopy, Size); RunningUserCallback = false; UnitStopTime = system_clock::now(); - (void)Res; - assert(Res == 0); + assert(CBRes == 0 || CBRes == -1); HasMoreMallocsThanFrees = AllocTracer.Stop(); } if (!LooseMemeq(DataCopy, Data, Size)) CrashOnOverwrittenData(); CurrentUnitSize = 0; delete[] DataCopy; + return CBRes == 0; } std::string Fuzzer::WriteToOutputCorpus(const Unit &U) { @@ -843,9 +844,16 @@ void Fuzzer::ReadAndExecuteSeedCorpora(std::vector &CorporaFiles) { } if (Corpus.empty() && Options.MaxNumberOfRuns) { - Printf("ERROR: no interesting inputs were found. " - "Is the code instrumented for coverage? Exiting.\n"); - exit(1); + Printf("WARNING: no interesting inputs were found so far. " + "Is the code instrumented for coverage?\n" + "This may also happen if the target rejected all inputs we tried so " + "far\n"); + // The remaining logic requires that the corpus is not empty, + // so we add one fake input to the in-memory corpus. + Corpus.AddToCorpus({'\n'}, /*NumFeatures=*/1, /*MayDeleteFile=*/true, + /*HasFocusFunction=*/false, /*NeverReduce=*/false, + /*TimeOfUnit=*/duration_cast(0s), {0}, DFT, + /*BaseII*/ nullptr); } } diff --git a/compiler-rt/test/fuzzer/Reject.cpp b/compiler-rt/test/fuzzer/Reject.cpp new file mode 100644 index 00000000000000..d67085cd50102e --- /dev/null +++ b/compiler-rt/test/fuzzer/Reject.cpp @@ -0,0 +1,23 @@ +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +// Tests how the fuzzer rejects inputs if the target returns -1. +#include +#include + +static volatile int Sink; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + if (Size != 3) + return -1; // Reject anyting that's not 3 bytes long. + // Reject 'rej'. + if (Data[0] == 'r' && Data[1] == 'e' && Data[2] == 'j') + return -1; + // Accept 'acc'. + if (Data[0] == 'a' && Data[1] == 'c' && Data[2] == 'c') { + Sink = 1; + return 0; + } + return 0; +} diff --git a/compiler-rt/test/fuzzer/not-instrumented.test b/compiler-rt/test/fuzzer/not-instrumented.test index 2330c477006755..6bf51f05c0f26a 100644 --- a/compiler-rt/test/fuzzer/not-instrumented.test +++ b/compiler-rt/test/fuzzer/not-instrumented.test @@ -1,4 +1,4 @@ RUN: %cpp_compiler %S/NotinstrumentedTest.cpp -fsanitize-coverage=0 -o %t-NotinstrumentedTest-NoCoverage -RUN: not %run %t-NotinstrumentedTest-NoCoverage 2>&1 | FileCheck %s --check-prefix=NO_COVERAGE +RUN: %run %t-NotinstrumentedTest-NoCoverage -runs=100 2>&1 | FileCheck %s --check-prefix=NO_COVERAGE -NO_COVERAGE: ERROR: no interesting inputs were found. Is the code instrumented for coverage? Exiting +NO_COVERAGE: WARNING: no interesting inputs were found so far. Is the code instrumented for coverage? diff --git a/compiler-rt/test/fuzzer/reject.test b/compiler-rt/test/fuzzer/reject.test new file mode 100644 index 00000000000000..b7b7391ba89e17 --- /dev/null +++ b/compiler-rt/test/fuzzer/reject.test @@ -0,0 +1,9 @@ +# Runs the Reject.cpp test, +# ensures that the input 'acc' is present in the corpus, +# and the input 'rej' is not. + +RUN: rm -rf %t-corpus && mkdir %t-corpus +RUN: %cpp_compiler %S/Reject.cpp -o %t-Reject +RUN: %run %t-Reject -runs=1000000 %t-corpus +RUN: grep 'acc' %t-corpus/* +RUN: not grep 'rej' %t-corpus/* diff --git a/llvm/docs/LibFuzzer.rst b/llvm/docs/LibFuzzer.rst index d4374602e7c1ac..89265ebfe35ecc 100644 --- a/llvm/docs/LibFuzzer.rst +++ b/llvm/docs/LibFuzzer.rst @@ -49,7 +49,7 @@ Like this: // fuzz_target.cc extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { DoSomethingInterestingWithMyAPI(Data, Size); - return 0; // Non-zero return values are reserved for future use. + return 0; // Values other than 0 and -1 are reserved for future use. } Note that this fuzz target does not depend on libFuzzer in any way @@ -646,6 +646,28 @@ arguments and a callback. This callback is invoked just like int (*UserCb)(const uint8_t *Data, size_t Size)); +Rejecting unwanted inputs +------------------------- + +It may be desirable to reject some inputs, i.e. to not add them to the corpus. + +For example, when fuzzing an API consisting of parsing and other logic, +one may want to allow only those inputs into the corpus that parse successfully. + +If the fuzz target returns -1 on a given input, +libFuzzer will not add that input top the corpus, regardless of what coverage +it triggers. + + +.. code-block:: c++ + + extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + if (auto *Obj = ParseMe(Data, Size)) { + Obj->DoSomethingInteresting(); + return 0; // Accept. The input may be added to the corpus. + } + return -1; // Reject; The input will not be added to the corpus. + } Leaks -----