diff --git a/packages/core/core.ts b/packages/core/core.ts index 64f1b4f2..5609a0e9 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -25,16 +25,16 @@ import * as reports from "istanbul-reports"; import * as fuzzer from "@jazzer.js/fuzzer"; import * as hooking from "@jazzer.js/hooking"; import { - loadBugDetectors, + clearFirstFinding, Finding, getFirstFinding, - clearFirstFinding, + loadBugDetectors, } from "@jazzer.js/bug-detectors"; import { - registerInstrumentor, - Instrumentor, FileSyncIdStrategy, + Instrumentor, MemorySyncIdStrategy, + registerInstrumentor, } from "@jazzer.js/instrumentor"; import { builtinModules } from "module"; @@ -404,16 +404,32 @@ export function wrapFuzzFunctionForBugDetection( let result: void | Promise; try { result = (originalFuzzFn as fuzzer.FuzzTargetAsyncOrValue)(data); + // Explicitly set promise handlers to process findings, but still return + // the fuzz target result directly, so that sync execution is still + // possible. + if (result instanceof Promise) { + result = result.then( + (result) => { + return throwIfError() ?? result; + }, + (reason) => { + return throwIfError(reason); + } + ); + } } catch (e) { fuzzTargetError = e; } - return handleErrors(result, fuzzTargetError); + return throwIfError(fuzzTargetError) ?? result; }; } else { - return (data: Buffer, done: (err?: Error) => void): void => { - let fuzzTargetError: unknown; + return ( + data: Buffer, + done: (err?: Error) => void + ): void | Promise => { try { - originalFuzzFn(data, (err?: Error) => { + // Return result of fuzz target to enable sanity checks in C++ part. + return originalFuzzFn(data, (err?: Error) => { const finding = getFirstFinding(); if (finding !== undefined) { clearFirstFinding(); @@ -421,23 +437,22 @@ export function wrapFuzzFunctionForBugDetection( done(finding ?? err); }); } catch (e) { - fuzzTargetError = e; + throwIfError(e); } - handleErrors(undefined, fuzzTargetError); }; } } -function handleErrors(result: void | Promise, fuzzTargetError: unknown) { +function throwIfError(fuzzTargetError?: unknown) { const error = getFirstFinding(); if (error !== undefined) { // The `firstFinding` is a global variable: we need to clear it after each fuzzing iteration. clearFirstFinding(); throw error; - } else if (fuzzTargetError !== undefined) { + } else if (fuzzTargetError) { throw fuzzTargetError; } - return result; + return undefined; } async function importModule(name: string): Promise { diff --git a/tests/bug-detectors/bug-detectors.test.js b/tests/bug-detectors/bug-detectors.test.js index 273c8282..a9b05367 100644 --- a/tests/bug-detectors/bug-detectors.test.js +++ b/tests/bug-detectors/bug-detectors.test.js @@ -37,7 +37,7 @@ describe("General tests", () => { it("Call with EVIL string; ASYNC", () => { const fuzzTest = new FuzzTestBuilder() - .sync(false) + .runs(0) .fuzzEntryPoint("CallOriginalEvilAsync") .dir(bugDetectorDirectory) .build(); diff --git a/tests/bug-detectors/general/fuzz.js b/tests/bug-detectors/general/fuzz.js index 5c4078a8..83755965 100644 --- a/tests/bug-detectors/general/fuzz.js +++ b/tests/bug-detectors/general/fuzz.js @@ -25,9 +25,26 @@ const friendlyFile = "FRIENDLY"; const friendlyCommand = (process.platform === "win32" ? "copy NUL " : "touch ") + friendlyFile; +let evilAsyncInvocations = 0; // eslint-disable-next-line @typescript-eslint/no-unused-vars module.exports.CallOriginalEvilAsync = async function (data) { - child_process.execSync(evilCommand); + return new Promise((resolve) => { + // Fuzz target is invoked two times. Skip the first one to verify that the + // fuzzer reports the async finding of the second invocation. + if (evilAsyncInvocations++ === 0) { + resolve(); + return; + } + setTimeout(() => { + try { + child_process.execSync(evilCommand); + } catch (ignored) { + // Swallow exception to force out of band notification of finding. + } finally { + resolve(); + } + }, 100); + }); }; // eslint-disable-next-line @typescript-eslint/no-unused-vars