diff --git a/packages/core/core.ts b/packages/core/core.ts index e2f31392..c3bc62f1 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 Code Intelligence GmbH + * Copyright 2023 Code Intelligence GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ * limitations under the License. */ import path from "path"; -import * as process from "process"; import * as tmp from "tmp"; import * as fs from "fs"; @@ -169,14 +168,40 @@ export async function startFuzzingNoInit( fuzzFn: fuzzer.FuzzTarget, options: Options, ) { + // Signal handler that stops fuzzing when the process receives a SIGINT, + // necessary to generate coverage reports and print debug information. + // The handler stops the process via `stopFuzzing`, as resolving the "fuzzing + // promise" does not work in sync mode due to the blocked event loop. + const signalHandler = () => { + stopFuzzing( + undefined, + options.expectedErrors, + options.coverageDirectory, + options.coverageReporters, + options.sync, + 0, + ); + }; + const fuzzerOptions = buildFuzzerOptions(options); logInfoAboutFuzzerOptions(fuzzerOptions); - const fuzzerFn = options.sync - ? Fuzzer.startFuzzing - : Fuzzer.startFuzzingAsync; - // Wrap the potentially sync fuzzer call, so that resolve and exception - // handlers are always executed. - return Promise.resolve().then(() => fuzzerFn(fuzzFn, fuzzerOptions)); + + if (options.sync) { + return Promise.resolve().then(() => + Fuzzer.startFuzzing( + fuzzFn, + fuzzerOptions, + // In synchronous mode, we cannot use the SIGINT handler in Node, + // because it won't be called until the fuzzing process is finished. + // Hence, we pass a callback function to the native fuzzer. + signalHandler, + ), + ); + } else { + // Add a Node SIGINT handler to stop fuzzing gracefully. + process.on("SIGINT", signalHandler); + return Fuzzer.startFuzzingAsync(fuzzFn, fuzzerOptions); + } } function prepareLibFuzzerArg0(fuzzerOptions: string[]): string { @@ -238,6 +263,7 @@ function stopFuzzing( coverageDirectory: string, coverageReporters: reports.ReportType[], sync: boolean, + forceShutdownWithCode?: number, ) { const stopFuzzing = sync ? Fuzzer.stopFuzzing : Fuzzer.stopFuzzingAsync; if (process.env.JAZZER_DEBUG) { @@ -257,13 +283,15 @@ function stopFuzzing( ); } - // No error found, check if one is expected. + // No error found, check if one is expected or an exit code should be enforced. if (!err) { if (expectedErrors.length) { console.error( `ERROR: Received no error, but expected one of [${expectedErrors}].`, ); stopFuzzing(ERROR_UNEXPECTED_CODE); + } else if (forceShutdownWithCode !== undefined) { + stopFuzzing(forceShutdownWithCode); } return; } @@ -377,6 +405,11 @@ function buildFuzzerOptions(options: Options): string[] { } const inSeconds = Math.ceil(options.timeout / 1000); opts = opts.concat(`-timeout=${inSeconds}`); + + // libFuzzer has to ignore SIGINT and SIGTERM, as it interferes + // with the Node.js signal handling. + opts = opts.concat("-handle_int=0", "-handle_term=0"); + return [prepareLibFuzzerArg0(opts), ...opts]; } diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index bd6d9cc5..60b9e2c5 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -27,6 +27,7 @@ export type FuzzOpts = string[]; export type StartFuzzingSyncFn = ( fuzzFn: FuzzTarget, fuzzOpts: FuzzOpts, + sigintCallback: () => void, ) => void; export type StartFuzzingAsyncFn = ( fuzzFn: FuzzTarget, diff --git a/packages/fuzzer/fuzzing_sync.cpp b/packages/fuzzer/fuzzing_sync.cpp index 577db48c..5bd68cc2 100644 --- a/packages/fuzzer/fuzzing_sync.cpp +++ b/packages/fuzzer/fuzzing_sync.cpp @@ -1,4 +1,4 @@ -// Copyright 2022 Code Intelligence GmbH +// Copyright 2023 Code Intelligence GmbH // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ #include "fuzzing_sync.h" #include "shared/libfuzzer.h" #include "utils.h" +#include #include #include @@ -23,14 +24,22 @@ namespace { struct FuzzTargetInfo { Napi::Env env; Napi::Function target; + Napi::Function stopFunction; }; // The JS fuzz target. We need to store the function pointer in a global // variable because libfuzzer doesn't give us a way to feed user-provided data // to its target function. std::optional gFuzzTarget; + +// Track if SIGINT signal handler was called. +// This is only necessary in the sync fuzzing case, as async can be handled +// much nicer directly in JavaScript. +volatile std::sig_atomic_t gSignalStatus; } // namespace +void sigintHandler(int signum) { gSignalStatus = signum; } + // The libFuzzer callback when fuzzing synchronously int FuzzCallbackSync(const uint8_t *Data, size_t Size) { // Create a new active scope so that handles for the buffer objects created in @@ -60,6 +69,12 @@ int FuzzCallbackSync(const uint8_t *Data, size_t Size) { } else { SyncReturnsHandler(); } + + // Execute the signal handler in context of the node application. + if (gSignalStatus != 0) { + gFuzzTarget->stopFunction.Call({}); + } + return EXIT_SUCCESS; } @@ -71,17 +86,24 @@ int FuzzCallbackSync(const uint8_t *Data, size_t Size) { // parameter; the fuzz target's return value is ignored. The second argument // is an array of (command-line) arguments to pass to libfuzzer. void StartFuzzing(const Napi::CallbackInfo &info) { - if (info.Length() != 2 || !info[0].IsFunction() || !info[1].IsArray()) { - throw Napi::Error::New(info.Env(), - "Need two arguments, which must be the fuzz target " - "function and an array of libfuzzer arguments"); + if (info.Length() != 3 || !info[0].IsFunction() || !info[1].IsArray() || + !info[2].IsFunction()) { + throw Napi::Error::New( + info.Env(), + "Need three arguments, which must be the fuzz target " + "function, an array of libfuzzer arguments, and a callback function " + "that the fuzzer will call in case of SIGINT or a segmentation fault"); } auto fuzzer_args = LibFuzzerArgs(info.Env(), info[1].As()); // Store the JS fuzz target and corresponding environment globally, so that - // our C++ fuzz target can use them to call back into JS. - gFuzzTarget = {info.Env(), info[0].As()}; + // our C++ fuzz target can use them to call back into JS. Also store the stop + // function that will be called in case of a SIGINT. + gFuzzTarget = {info.Env(), info[0].As(), + info[2].As()}; + + signal(SIGINT, sigintHandler); StartLibFuzzer(fuzzer_args, FuzzCallbackSync); // Explicitly reset the global function pointer because the JS @@ -93,9 +115,9 @@ void StartFuzzing(const Napi::CallbackInfo &info) { void StopFuzzing(const Napi::CallbackInfo &info) { int exitCode = StopFuzzingHandleExit(info); - // If we ran in async mode and we only ever encountered synchronous results + // If we ran in async mode, and we only ever encountered synchronous results // we'll give an indicator that running in synchronous mode is likely - // benefical + // beneficial. ReturnValueInfo(true); // We call _Exit to immediately terminate the process without performing any diff --git a/packages/fuzzer/utils.cpp b/packages/fuzzer/utils.cpp index 6aec2b08..804efb3a 100644 --- a/packages/fuzzer/utils.cpp +++ b/packages/fuzzer/utils.cpp @@ -1,4 +1,4 @@ -// Copyright 2022 Code Intelligence GmbH +// Copyright 2023 Code Intelligence GmbH // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,8 +17,6 @@ #include "shared/libfuzzer.h" #include -#define btoa(x) ((x) ? "true" : "false") - void StartLibFuzzer(const std::vector &args, fuzzer::UserCallback fuzzCallback) { std::vector fuzzer_arg_pointers; diff --git a/tests/bug-detectors/command-injection.test.js b/tests/bug-detectors/command-injection.test.js index 43e94936..ae5fd2b1 100644 --- a/tests/bug-detectors/command-injection.test.js +++ b/tests/bug-detectors/command-injection.test.js @@ -19,7 +19,7 @@ const { FuzzTestBuilder, FuzzingExitCode, // eslint-disable-next-line @typescript-eslint/no-var-requires -} = require("./helpers.js"); +} = require("../helpers.js"); // eslint-disable-next-line @typescript-eslint/no-var-requires const path = require("path"); // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/tests/bug-detectors/general.test.js b/tests/bug-detectors/general.test.js index ab1058c4..533587cf 100644 --- a/tests/bug-detectors/general.test.js +++ b/tests/bug-detectors/general.test.js @@ -20,7 +20,7 @@ const { FuzzingExitCode, JestRegressionExitCode, // eslint-disable-next-line @typescript-eslint/no-var-requires -} = require("./helpers.js"); +} = require("../helpers.js"); // eslint-disable-next-line @typescript-eslint/no-var-requires const path = require("path"); // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/tests/bug-detectors/general/fuzz.js b/tests/bug-detectors/general/fuzz.js index cebb07fb..50954401 100644 --- a/tests/bug-detectors/general/fuzz.js +++ b/tests/bug-detectors/general/fuzz.js @@ -25,7 +25,7 @@ const fs = require("fs"); const assert = require("assert"); const { platform } = require("os"); -const { makeFnCalledOnce, callWithTimeout } = require("../helpers"); +const { makeFnCalledOnce, callWithTimeout } = require("../../helpers"); const evilCommand = "jaz_zer"; const friendlyFile = "FRIENDLY"; diff --git a/tests/bug-detectors/path-traversal.test.js b/tests/bug-detectors/path-traversal.test.js index 4553fd3d..ed7d7a96 100644 --- a/tests/bug-detectors/path-traversal.test.js +++ b/tests/bug-detectors/path-traversal.test.js @@ -19,7 +19,7 @@ const { FuzzTestBuilder, FuzzingExitCode, // eslint-disable-next-line @typescript-eslint/no-var-requires -} = require("./helpers.js"); +} = require("../helpers.js"); // eslint-disable-next-line @typescript-eslint/no-var-requires const path = require("path"); // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/tests/bug-detectors/path-traversal/fuzz.js b/tests/bug-detectors/path-traversal/fuzz.js index c07cdfe0..4f01f166 100644 --- a/tests/bug-detectors/path-traversal/fuzz.js +++ b/tests/bug-detectors/path-traversal/fuzz.js @@ -23,7 +23,7 @@ const fs = require("fs"); const fsp = require("fs").promises; const path = require("path"); -const { makeFnCalledOnce } = require("../helpers"); +const { makeFnCalledOnce } = require("../../helpers"); const evil_path = "../../jaz_zer/"; const safe_path = "../../safe_path/"; diff --git a/tests/bug-detectors/helpers.js b/tests/helpers.js similarity index 91% rename from tests/bug-detectors/helpers.js rename to tests/helpers.js index c617e51a..a994f602 100644 --- a/tests/bug-detectors/helpers.js +++ b/tests/helpers.js @@ -23,10 +23,11 @@ const path = require("path"); // eslint-disable-next-line @typescript-eslint/no-var-requires const assert = require("assert"); -// This is used to distinguish an error thrown during fuzzing from other errors, such as wrong -// `fuzzEntryPoint` (which would return a "1") +// This is used to distinguish an error thrown during fuzzing from other errors, +// such as wrong `fuzzEntryPoint`, which would return a "1". const FuzzingExitCode = "77"; const JestRegressionExitCode = "1"; +const WindowsExitCode = "1"; class FuzzTest { sync; @@ -40,6 +41,7 @@ class FuzzTest { jestTestFile; jestTestNamePattern; jestRunInFuzzingMode; + coverage; constructor( sync, @@ -53,6 +55,7 @@ class FuzzTest { jestTestFile, jestTestName, jestRunInFuzzingMode, + coverage, ) { this.sync = sync; this.runs = runs; @@ -65,6 +68,7 @@ class FuzzTest { this.jestTestFile = jestTestFile; this.jestTestNamePattern = jestTestName; this.jestRunInFuzzingMode = jestRunInFuzzingMode; + this.coverage = coverage; } execute() { @@ -78,6 +82,7 @@ class FuzzTest { for (const bugDetector of this.disableBugDetectors) { options.push("--disable_bug_detectors=" + bugDetector); } + if (this.coverage) options.push("--coverage"); options.push("--"); options.push("-runs=" + this.runs); if (this.forkMode) options.push("-fork=" + this.forkMode); @@ -101,6 +106,7 @@ class FuzzTest { const cmd = "npx"; const options = [ "jest", + this.coverage ? "--coverage" : "", this.jestTestFile, '--testNamePattern="' + this.jestTestNamePattern + '"', ]; @@ -145,6 +151,7 @@ class FuzzTestBuilder { _jestTestFile = ""; _jestTestName = ""; _jestRunInFuzzingMode = false; + _coverage = false; /** * @param {boolean} sync - whether to run the fuzz test in synchronous mode. @@ -241,6 +248,11 @@ class FuzzTestBuilder { return this; } + coverage(coverage) { + this._coverage = coverage; + return this; + } + build() { if (this._jestTestFile === "" && this._fuzzEntryPoint === "") { throw new Error("fuzzEntryPoint or jestTestFile are not set."); @@ -262,6 +274,7 @@ class FuzzTestBuilder { this._jestTestFile, this._jestTestName, this._jestRunInFuzzingMode, + this._coverage, ); } } @@ -308,8 +321,19 @@ function callWithTimeout(fn, timeout) { }); } +/** + * Returns a Jest describe function that is skipped if the current platform is not the given one. + * @param platform + * @returns describe(.skip) function + */ +function describeSkipOnPlatform(platform) { + return process.platform === platform ? global.describe.skip : global.describe; +} + module.exports.FuzzTestBuilder = FuzzTestBuilder; module.exports.FuzzingExitCode = FuzzingExitCode; module.exports.JestRegressionExitCode = JestRegressionExitCode; +module.exports.WindowsExitCode = WindowsExitCode; module.exports.makeFnCalledOnce = makeFnCalledOnce; module.exports.callWithTimeout = callWithTimeout; +module.exports.describeSkipOnPlatform = describeSkipOnPlatform; diff --git a/tests/signal_handlers/SIGINT/.gitignore b/tests/signal_handlers/SIGINT/.gitignore new file mode 100644 index 00000000..d39943c3 --- /dev/null +++ b/tests/signal_handlers/SIGINT/.gitignore @@ -0,0 +1,2 @@ +tests.fuzz +.jazzerjsrc.json diff --git a/tests/signal_handlers/SIGINT/fuzz.js b/tests/signal_handlers/SIGINT/fuzz.js new file mode 100644 index 00000000..ab76313e --- /dev/null +++ b/tests/signal_handlers/SIGINT/fuzz.js @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +let i = 0; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +module.exports.SIGINT_SYNC = (data) => { + if (i === 1000) { + console.log("kill with SIGINT"); + process.kill(process.pid, "SIGINT"); + } + if (i > 1000) { + console.log("SIGINT has not stopped the fuzzing process"); + } + i++; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +module.exports.SIGINT_ASYNC = (data) => { + // Raising SIGINT in async mode does not stop the fuzzer directly, + // as the event is handled asynchronously in the event loop. + if (i === 1000) { + console.log("kill with SIGINT"); + process.kill(process.pid, "SIGINT"); + } + i++; +}; diff --git a/tests/signal_handlers/SIGINT/package.json b/tests/signal_handlers/SIGINT/package.json new file mode 100644 index 00000000..80715365 --- /dev/null +++ b/tests/signal_handlers/SIGINT/package.json @@ -0,0 +1,26 @@ +{ + "name": "jazzerjs-signal-handler-tests", + "version": "1.0.0", + "description": "Tests for the SIGINT signal handler", + "scripts": { + "test": "jest", + "fuzz": "JAZZER_FUZZ=1 jest" + }, + "devDependencies": { + "@jazzer.js/jest-runner": "file:../../packages/jest-runner" + }, + "jest": { + "projects": [ + { + "runner": "@jazzer.js/jest-runner", + "displayName": { + "name": "Jazzer.js", + "color": "cyan" + }, + "testMatch": [ + "/**/*.fuzz.js" + ] + } + ] + } +} diff --git a/tests/signal_handlers/SIGINT/tests.fuzz.js b/tests/signal_handlers/SIGINT/tests.fuzz.js new file mode 100644 index 00000000..238d4c29 --- /dev/null +++ b/tests/signal_handlers/SIGINT/tests.fuzz.js @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint no-undef: 0 */ +/* eslint-disable @typescript-eslint/no-var-requires */ + +const { SIGINT_SYNC, SIGINT_ASYNC } = require("./fuzz.js"); + +describe("Jest", () => { + it.fuzz("Sync", SIGINT_SYNC); + it.fuzz("Async", SIGINT_ASYNC); +}); diff --git a/tests/signal_handlers/package.json b/tests/signal_handlers/package.json new file mode 100644 index 00000000..ac9507a5 --- /dev/null +++ b/tests/signal_handlers/package.json @@ -0,0 +1,12 @@ +{ + "name": "jazzer-js-tests-for-signal-handlers", + "version": "1.0.0", + "description": "Tests for Jazzer.js' signal handlers", + "scripts": { + "fuzz": "jest --verbose", + "test": "jest --verbose" + }, + "devDependencies": { + "@jazzer.js/core": "file:../../packages/core" + } +} diff --git a/tests/signal_handlers/signal_handlers.test.js b/tests/signal_handlers/signal_handlers.test.js new file mode 100644 index 00000000..e58a3614 --- /dev/null +++ b/tests/signal_handlers/signal_handlers.test.js @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Code Intelligence GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint no-undef: 0 */ +const { + FuzzTestBuilder, + describeSkipOnPlatform, + // eslint-disable-next-line @typescript-eslint/no-var-requires +} = require("../helpers.js"); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const path = require("path"); + +// Signal handling in Node.js on Windows is only rudimentary supported. +// Specifically using `process.kill`, like the test does to interrupt itself, +// will unconditionally terminate the process. The signal processing works in +// manual tests, though. +const describe = describeSkipOnPlatform("win32"); + +describe("SIGINT handlers", () => { + let fuzzTestBuilder; + + beforeEach(() => { + fuzzTestBuilder = new FuzzTestBuilder() + .runs(2000) + .dir(path.join(__dirname, "SIGINT")) + .coverage(true) + .verbose(true); + }); + + describe("in standalone fuzzing mode", () => { + it("stop sync fuzzing on SIGINT", () => { + const fuzzTest = fuzzTestBuilder + .sync(true) + .fuzzEntryPoint("SIGINT_SYNC") + .build(); + fuzzTest.execute(); + assertSigintMessagesLogged(fuzzTest); + }); + it("stop async fuzzing on SIGINT", () => { + const fuzzTest = fuzzTestBuilder + .sync(false) + .fuzzEntryPoint("SIGINT_ASYNC") + .build(); + fuzzTest.execute(); + assertSigintMessagesLogged(fuzzTest); + }); + }); + + describe("in Jest fuzzing mode", () => { + it("stop sync fuzzing on SIGINT", () => { + const fuzzTest = fuzzTestBuilder + .jestTestFile("tests.fuzz.js") + .jestTestName("^Jest Sync$") + .jestRunInFuzzingMode(true) + .build(); + fuzzTest.execute(); + assertSigintMessagesLogged(fuzzTest); + }); + it("stop async fuzzing on SIGINT", () => { + const fuzzTest = fuzzTestBuilder + .jestTestFile("tests.fuzz.js") + .jestTestName("^Jest Async$") + .jestRunInFuzzingMode(true) + .build(); + fuzzTest.execute(); + assertSigintMessagesLogged(fuzzTest); + }); + }); +}); + +function assertSigintMessagesLogged(fuzzTest) { + expect(fuzzTest.stdout).toContain("kill with SIGINT"); + + // We asked for a coverage report. Here we only look for the universal part of its header. + expect(fuzzTest.stdout).toContain( + "| % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s", + ); + + // "SIGINT handler called more than once" should not be printed in sync mode. + expect(fuzzTest.stdout).not.toContain( + "SIGINT has not stopped the fuzzing process", + ); +}