4848import java .util .HashSet ;
4949import java .util .List ;
5050import java .util .Locale ;
51+ import java .util .Objects ;
5152import java .util .Set ;
52- import java .util .function .Predicate ;
53+ import java .util .function .Consumer ;
5354import java .util .stream .Stream ;
5455import sun .misc .Unsafe ;
5556
@@ -118,7 +119,7 @@ public final class FuzzTargetRunner {
118119 private static final boolean useFuzzedDataProvider ;
119120 private static final ArgumentsMutator mutator ;
120121 private static final ReproducerTemplate reproducerTemplate ;
121- private static Predicate <Throwable > findingHandler ;
122+ private static Consumer <Throwable > fatalFindingHandlerForJUnit ;
122123
123124 static {
124125 FuzzTargetHolder .FuzzTarget fuzzTarget = FuzzTargetHolder .fuzzTarget ;
@@ -136,7 +137,6 @@ public final class FuzzTargetRunner {
136137 if (!useFuzzedDataProvider && IS_ANDROID ) {
137138 Log .error ("Android fuzz targets must use " + FuzzedDataProvider .class .getName ());
138139 exit (1 );
139- throw new IllegalStateException ("Not reached" );
140140 }
141141
142142 Class <?> fuzzTargetClass = fuzzTarget .method .getDeclaringClass ();
@@ -147,9 +147,8 @@ public final class FuzzTargetRunner {
147147 try {
148148 lifecycleMethodsInvoker .beforeFirstExecution ();
149149 } catch (Throwable t ) {
150- Log .finding (t );
150+ Log .finding (ExceptionUtils . preprocessThrowable ( t ) );
151151 exit (1 );
152- throw new IllegalStateException ("Not reached" );
153152 }
154153
155154 if (useExperimentalMutator ) {
@@ -166,11 +165,7 @@ public final class FuzzTargetRunner {
166165 CoverageRecorder .updateCoveredIdsWithCoverageMap ();
167166 }
168167
169- // When running with a custom finding handler, such as from within JUnit, we can't reason about
170- // when the JVM shuts down and thus don't use shutdown handlers.
171- if (findingHandler == null ) {
172- Runtime .getRuntime ().addShutdownHook (new Thread (FuzzTargetRunner ::shutdown ));
173- }
168+ Runtime .getRuntime ().addShutdownHook (new Thread (FuzzTargetRunner ::shutdown ));
174169 }
175170
176171 /** A test-only convenience wrapper around {@link #runOne(long, int)}. */
@@ -215,7 +210,6 @@ private static int runOne(long dataPtr, int dataLength) {
215210 data = copyToArray (dataPtr , dataLength );
216211 argument = data ;
217212 }
218- Object fuzzTargetInstance ;
219213 try {
220214 lifecycleMethodsInvoker .beforeEachExecution ();
221215 } catch (Throwable uncaughtFinding ) {
@@ -225,7 +219,7 @@ private static int runOne(long dataPtr, int dataLength) {
225219 // always enter the try block that calls afterEachExecution in finally.
226220 if (finding == null ) {
227221 try {
228- fuzzTargetInstance = lifecycleMethodsInvoker .getTestClassInstance ();
222+ Object fuzzTargetInstance = lifecycleMethodsInvoker .getTestClassInstance ();
229223 if (useExperimentalMutator ) {
230224 // No need to detach as we are currently reading in the mutator state from bytes in every
231225 // iteration.
@@ -237,7 +231,7 @@ private static int runOne(long dataPtr, int dataLength) {
237231 }
238232 } catch (Throwable uncaughtFinding ) {
239233 finding = uncaughtFinding ;
240- } finally {
234+ } finally {
241235 try {
242236 lifecycleMethodsInvoker .afterEachExecution ();
243237 } catch (Throwable t ) {
@@ -272,6 +266,12 @@ private static int runOne(long dataPtr, int dataLength) {
272266 if (finding == null || finding .getClass ().getName ().equals (OPENTEST4J_TEST_ABORTED_EXCEPTION )) {
273267 return LIBFUZZER_CONTINUE ;
274268 }
269+
270+ // The user-provided fuzz target method has returned. Any further exits, e.g. due to uncaught
271+ // exceptions, are on us and should not result in a "fuzz target exited" warning being printed
272+ // by libFuzzer.
273+ temporarilyDisableLibfuzzerExitHook ();
274+
275275 if (useHooks ) {
276276 finding = ExceptionUtils .preprocessThrowable (finding );
277277 }
@@ -280,54 +280,44 @@ private static int runOne(long dataPtr, int dataLength) {
280280 if (emitDedupToken && !ignoredTokens .add (dedupToken )) {
281281 return LIBFUZZER_CONTINUE ;
282282 }
283-
284- if (findingHandler != null ) {
285- // We still print the libFuzzer crashing input information, which also dumps the crashing
286- // input as a side effect.
287- printCrashingInput ();
288- if (findingHandler .test (finding )) {
289- return LIBFUZZER_CONTINUE ;
290- } else {
291- try {
292- // We have to call afterLastExecution here as we do not register the shutdown hook that
293- // would otherwise call it when findingHandler != null.
294- lifecycleMethodsInvoker .afterLastExecution ();
295- } catch (Throwable t ) {
296- // We already have a finding and do not know whether the fuzz target is in an expected
297- // state, so report this as a warning rather than an error or finding.
298- Log .warn ("Failed to run @AfterAll or fuzzerTearDown methods" , t );
299- }
300- return LIBFUZZER_RETURN_FROM_DRIVER ;
301- }
283+ boolean continueFuzzing =
284+ emitDedupToken && Long .compareUnsigned (ignoredTokens .size (), keepGoing ) < 0 ;
285+ boolean isFuzzingFromCommandLine =
286+ fatalFindingHandlerForJUnit == null || Opt .isJUnitAndCommandLine .get ();
287+ // In case of --keep_going, only the last finding is reported to JUnit as a Java object, all
288+ // previous ones are merely printed. When fuzzing from the command line, we always print all
289+ // findings.
290+ if (isFuzzingFromCommandLine || continueFuzzing ) {
291+ Log .finding (finding );
292+ }
293+ if (fatalFindingHandlerForJUnit != null && !continueFuzzing ) {
294+ fatalFindingHandlerForJUnit .accept (finding );
302295 }
303-
304- // The user-provided fuzz target method has returned. Any further exits are on us and should not
305- // result in a "fuzz target exited" warning being printed by libFuzzer.
306- temporarilyDisableLibfuzzerExitHook ();
307-
308- Log .finding (finding );
309296 if (emitDedupToken ) {
310297 // Has to be printed to stdout as it is parsed by libFuzzer when minimizing a crash. It does
311298 // not necessarily have to appear at the beginning of a line.
312299 // https://github.com/llvm/llvm-project/blob/4c106c93eb68f8f9f201202677cd31e326c16823/compiler-rt/lib/fuzzer/FuzzerDriver.cpp#L342
313300 Log .structuredOutput (String .format (Locale .ROOT , "DEDUP_TOKEN: %016x" , dedupToken ));
314301 }
315- Log .println ("== libFuzzer crashing input ==" );
316- printCrashingInput ();
302+ if (isFuzzingFromCommandLine ) {
303+ // We emit this line for backwards compatibility when fuzzing on the CLI only.
304+ Log .println ("== libFuzzer crashing input ==" );
305+ }
306+ printAndDumpCrashingInput ();
307+
317308 // dumpReproducer needs to be called after libFuzzer printed its final stats as otherwise it
318309 // would report incorrect coverage - the reproducer generation involved rerunning the fuzz
319310 // target.
320311 // It doesn't support @FuzzTest fuzz targets, but these come with an integrated regression test
321312 // that satisfies the same purpose.
322313 // It also doesn't support the experimental mutator yet as that requires implementing Java code
323314 // generation for mutators.
324- if (findingHandler == null && !useExperimentalMutator ) {
315+ if (fatalFindingHandlerForJUnit == null && !useExperimentalMutator ) {
325316 dumpReproducer (data );
326317 }
327318
328- if (!emitDedupToken || Long .compareUnsigned (ignoredTokens .size (), keepGoing ) >= 0 ) {
329- // Reached the maximum amount of findings to keep going for, crash after shutdown.
330- if (!Opt .autofuzz .get ().isEmpty () && Opt .dedup .get ()) {
319+ if (!continueFuzzing ) {
320+ if (!Opt .autofuzz .get ().isEmpty () && emitDedupToken ) {
331321 Log .println ("" );
332322 Log .info (
333323 String .format (
@@ -342,8 +332,15 @@ private static int runOne(long dataPtr, int dataLength) {
342332 Opt .autofuzzIgnore .get ().stream (), Stream .of (finding .getClass ().getName ()))
343333 .collect (joining ("," ))));
344334 }
345- System .exit (JAZZER_FINDING_EXIT_CODE );
346- throw new IllegalStateException ("Not reached" );
335+ if (fatalFindingHandlerForJUnit == null ) {
336+ // When running a legacy fuzzerTestOneInput test, exit now with the correct exit code.
337+ // This will trigger the shutdown hook that runs fuzzerTearDown.
338+ System .exit (JAZZER_FINDING_EXIT_CODE );
339+ } else {
340+ // When running within JUnit, pass control back to FuzzTestExecutor, which has received
341+ // the finding via the handler.
342+ return LIBFUZZER_RETURN_FROM_DRIVER ;
343+ }
347344 }
348345 return LIBFUZZER_CONTINUE ;
349346 }
@@ -435,15 +432,8 @@ public static int startLibFuzzer(List<String> args) {
435432 args .stream ().map (str -> str .getBytes (StandardCharsets .UTF_8 )).toArray (byte [][]::new ));
436433 }
437434
438- /**
439- * Registers a custom handler for findings.
440- *
441- * @param findingHandler a consumer for the finding that returns true if the fuzzer should
442- * continue fuzzing and false if it should return from {@link
443- * FuzzTargetRunner#startLibFuzzer(List)}.
444- */
445- public static void registerFindingHandler (Predicate <Throwable > findingHandler ) {
446- FuzzTargetRunner .findingHandler = findingHandler ;
435+ public static void registerFatalFindingHandlerForJUnit (Consumer <Throwable > findingHandler ) {
436+ FuzzTargetRunner .fatalFindingHandlerForJUnit = Objects .requireNonNull (findingHandler );
447437 }
448438
449439 private static void shutdown () {
@@ -556,8 +546,8 @@ private static int startLibFuzzer(byte[][] args) {
556546 * Causes libFuzzer to write the current input to disk as a crashing input and emit some
557547 * information about it to stderr.
558548 */
559- public static void printCrashingInput () {
560- FuzzTargetRunnerNatives .printCrashingInput ();
549+ public static void printAndDumpCrashingInput () {
550+ FuzzTargetRunnerNatives .printAndDumpCrashingInput ();
561551 }
562552
563553 /** Returns the debug string of the current mutator. If no mutator is used, returns null. */
0 commit comments