diff --git a/.gitignore b/.gitignore index fec9b17a5..b32959482 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ node_modules/ # Output of 'npm pack' *.tgz + +# Dictionaries generated by Jazzer.js +.JazzerJs-merged-dictionaries \ No newline at end of file diff --git a/packages/core/core.ts b/packages/core/core.ts index c63e925ee..3be231df6 100644 --- a/packages/core/core.ts +++ b/packages/core/core.ts @@ -23,7 +23,12 @@ import * as reports from "istanbul-reports"; import * as fuzzer from "@jazzer.js/fuzzer"; import * as hooking from "@jazzer.js/hooking"; -import { clearFirstFinding, getFirstFinding, printFinding } from "./finding"; +import { + clearFirstFinding, + getFirstFinding, + printFinding, + Finding, +} from "./finding"; import { FileSyncIdStrategy, Instrumentor, @@ -42,6 +47,8 @@ tmp.setGracefulCleanup(); const ERROR_EXPECTED_CODE = 0; const ERROR_UNEXPECTED_CODE = 78; +const SIGSEGV = 11; + export interface Options { // `fuzzTarget` is the name of an external module containing a `fuzzer.FuzzTarget` // that is resolved by `fuzzEntryPoint`. @@ -190,18 +197,18 @@ export async function startFuzzingNoInit( fuzzFn: fuzzer.FuzzTarget, options: Options, ) { - // Signal handler that stops fuzzing when the process receives a SIGINT, + // Signal handler that stops fuzzing when the process receives a SIGINT/SIGSEGV, // 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 = () => { + const signalHandler = (exitCode: number) => { stopFuzzing( undefined, options.expectedErrors, options.coverageDirectory, options.coverageReporters, options.sync, - 0, + exitCode, ); }; @@ -212,15 +219,16 @@ export async function startFuzzingNoInit( Fuzzer.startFuzzing( fuzzFn, fuzzerOptions, - // In synchronous mode, we cannot use the SIGINT handler in Node, + // In synchronous mode, we cannot use the SIGINT/SIGSEGV 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. + // The appropriate exitCode for the signalHandler will be added by the native fuzzer. signalHandler, ), ); } else { - // Add a Node SIGINT handler to stop fuzzing gracefully. - process.on("SIGINT", signalHandler); + process.on("SIGINT", () => signalHandler(0)); + process.on("SIGSEGV", () => signalHandler(SIGSEGV)); return Fuzzer.startFuzzingAsync(fuzzFn, fuzzerOptions); } } @@ -251,6 +259,11 @@ function stopFuzzing( ); } + // Prioritize findings over segfaults. + if (forceShutdownWithCode === SIGSEGV && !(err instanceof Finding)) { + err = new Finding("Segmentation Fault"); + } + // No error found, check if one is expected or an exit code should be enforced. if (!err) { if (expectedErrors.length) { @@ -258,7 +271,7 @@ function stopFuzzing( `ERROR: Received no error, but expected one of [${expectedErrors}].`, ); stopFuzzing(ERROR_UNEXPECTED_CODE); - } else if (forceShutdownWithCode !== undefined) { + } else if (forceShutdownWithCode === 0) { stopFuzzing(forceShutdownWithCode); } return; diff --git a/packages/core/options.ts b/packages/core/options.ts index d5daa0c72..423475785 100644 --- a/packages/core/options.ts +++ b/packages/core/options.ts @@ -33,7 +33,7 @@ export function buildFuzzerOption(options: Options) { // libFuzzer has to ignore SIGINT and SIGTERM, as it interferes // with the Node.js signal handling. - params = params.concat("-handle_int=0", "-handle_term=0"); + params = params.concat("-handle_int=0", "-handle_term=0", "-handle_segv=0"); if (process.env.JAZZER_DEBUG) { console.debug("DEBUG: [core] Jazzer.js actually used fuzzer arguments: "); diff --git a/packages/fuzzer/addon.ts b/packages/fuzzer/addon.ts index 60b9e2c5b..87bea915a 100644 --- a/packages/fuzzer/addon.ts +++ b/packages/fuzzer/addon.ts @@ -27,7 +27,7 @@ export type FuzzOpts = string[]; export type StartFuzzingSyncFn = ( fuzzFn: FuzzTarget, fuzzOpts: FuzzOpts, - sigintCallback: () => void, + jsStopCallback: (signal: number) => void, ) => void; export type StartFuzzingAsyncFn = ( fuzzFn: FuzzTarget, diff --git a/packages/fuzzer/fuzzing_sync.cpp b/packages/fuzzer/fuzzing_sync.cpp index 5bd68cc2d..69d7a727e 100644 --- a/packages/fuzzer/fuzzing_sync.cpp +++ b/packages/fuzzer/fuzzing_sync.cpp @@ -24,7 +24,7 @@ namespace { struct FuzzTargetInfo { Napi::Env env; Napi::Function target; - Napi::Function stopFunction; + Napi::Function jsStopCallback; // JS stop function used by signal handling. }; // The JS fuzz target. We need to store the function pointer in a global @@ -70,9 +70,16 @@ int FuzzCallbackSync(const uint8_t *Data, size_t Size) { SyncReturnsHandler(); } - // Execute the signal handler in context of the node application. if (gSignalStatus != 0) { - gFuzzTarget->stopFunction.Call({}); + // Non-zero exit codes will produce crash files. + auto exitCode = Napi::Number::New(gFuzzTarget->env, 0); + + if (gSignalStatus != SIGINT) { + exitCode = Napi::Number::New(gFuzzTarget->env, gSignalStatus); + } + + // Execute the signal handler in context of the node application. + gFuzzTarget->jsStopCallback.Call({exitCode}); } return EXIT_SUCCESS; @@ -99,11 +106,12 @@ void StartFuzzing(const Napi::CallbackInfo &info) { // Store the JS fuzz target and corresponding environment globally, so that // 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. + // function that will be called in case of a SIGINT/SIGSEGV. gFuzzTarget = {info.Env(), info[0].As(), info[2].As()}; signal(SIGINT, sigintHandler); + signal(SIGSEGV, sigintHandler); StartLibFuzzer(fuzzer_args, FuzzCallbackSync); // Explicitly reset the global function pointer because the JS diff --git a/packages/fuzzer/utils.cpp b/packages/fuzzer/utils.cpp index 804efb3a2..33212616d 100644 --- a/packages/fuzzer/utils.cpp +++ b/packages/fuzzer/utils.cpp @@ -15,6 +15,7 @@ #include "utils.h" #include "napi.h" #include "shared/libfuzzer.h" +#include #include void StartLibFuzzer(const std::vector &args, @@ -75,7 +76,7 @@ void ReturnValueInfo(bool is_sync_runner) { << "\n== Jazzer.js:\n" << " Exclusively observed synchronous return values from fuzzed " "function." - << " Fuzzing in synchronous mode seems benefical!\n" + << " Fuzzing in synchronous mode seems beneficial!\n" << " To enable it, append a `--sync` to your Jazzer.js invocation." << std::endl; } @@ -84,7 +85,7 @@ void ReturnValueInfo(bool is_sync_runner) { std::cerr << "\n== Jazzer.js:\n" << " Observed asynchronous return values from " "fuzzed function." - << " Fuzzing in asynchronous mode seems benefical!\n" + << " Fuzzing in asynchronous mode seems beneficial!\n" << " Remove the `--sync` flag from your Jazzer.js invocation." << std::endl; } @@ -96,6 +97,10 @@ int StopFuzzingHandleExit(const Napi::CallbackInfo &info) { if (info[0].IsNumber()) { exitCode = info[0].As().Int32Value(); + + if (exitCode == SIGSEGV) { + libfuzzer::PrintCrashingInput(); + } } else { // If a dedicated status code is provided, the run is executed as internal // test and the crashing input does not need to be printed/saved. diff --git a/tests/return_values/return_values.test.js b/tests/return_values/return_values.test.js index 662d0a2a7..3c2b3631f 100644 --- a/tests/return_values/return_values.test.js +++ b/tests/return_values/return_values.test.js @@ -17,9 +17,9 @@ const { spawnSync } = require("child_process"); const path = require("path"); const SyncInfo = - "Exclusively observed synchronous return values from fuzzed function. Fuzzing in synchronous mode seems benefical!"; + "Exclusively observed synchronous return values from fuzzed function. Fuzzing in synchronous mode seems beneficial!"; const AsyncInfo = - "Observed asynchronous return values from fuzzed function. Fuzzing in asynchronous mode seems benefical!"; + "Observed asynchronous return values from fuzzed function. Fuzzing in asynchronous mode seems beneficial!"; // current working directory const testDirectory = __dirname; diff --git a/tests/signal_handlers/SIGINT/fuzz.js b/tests/signal_handlers/SIGINT/fuzz.js index f0a009aad..a924a5ee7 100644 --- a/tests/signal_handlers/SIGINT/fuzz.js +++ b/tests/signal_handlers/SIGINT/fuzz.js @@ -18,11 +18,11 @@ let i = 0; module.exports.SIGINT_SYNC = (data) => { if (i === 1000) { - console.log("kill with SIGINT"); + console.log("kill with signal"); process.kill(process.pid, "SIGINT"); } if (i > 1000) { - console.log("SIGINT has not stopped the fuzzing process"); + console.log("Signal has not stopped the fuzzing process"); } i++; }; @@ -31,7 +31,7 @@ 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"); + console.log("kill with signal"); process.kill(process.pid, "SIGINT"); } i++; diff --git a/tests/signal_handlers/SIGSEGV/.gitignore b/tests/signal_handlers/SIGSEGV/.gitignore new file mode 100644 index 000000000..d39943c37 --- /dev/null +++ b/tests/signal_handlers/SIGSEGV/.gitignore @@ -0,0 +1,2 @@ +tests.fuzz +.jazzerjsrc.json diff --git a/tests/signal_handlers/SIGSEGV/fuzz.js b/tests/signal_handlers/SIGSEGV/fuzz.js new file mode 100644 index 000000000..b6ac855a8 --- /dev/null +++ b/tests/signal_handlers/SIGSEGV/fuzz.js @@ -0,0 +1,38 @@ +/* + * 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; + +module.exports.SIGSEGV_SYNC = (data) => { + if (i === 1000) { + console.log("kill with signal"); + process.kill(process.pid, "SIGSEGV"); + } + if (i > 1000) { + console.log("Signal has not stopped the fuzzing process"); + } + i++; +}; + +module.exports.SIGSEGV_ASYNC = (data) => { + // Raising SIGSEGV 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 signal"); + process.kill(process.pid, "SIGSEGV"); + } + i++; +}; diff --git a/tests/signal_handlers/SIGSEGV/package.json b/tests/signal_handlers/SIGSEGV/package.json new file mode 100644 index 000000000..80715365c --- /dev/null +++ b/tests/signal_handlers/SIGSEGV/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/SIGSEGV/tests.fuzz.js b/tests/signal_handlers/SIGSEGV/tests.fuzz.js new file mode 100644 index 000000000..8525e7129 --- /dev/null +++ b/tests/signal_handlers/SIGSEGV/tests.fuzz.js @@ -0,0 +1,22 @@ +/* + * 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. + */ + +const { SIGSEGV_ASYNC, SIGSEGV_SYNC } = require("./fuzz.js"); + +describe("Jest", () => { + it.fuzz("Sync", SIGSEGV_SYNC); + it.fuzz("Async", SIGSEGV_ASYNC); +}); diff --git a/tests/signal_handlers/signal_handlers.test.js b/tests/signal_handlers/signal_handlers.test.js index 46a24a0cc..6d62acf8a 100644 --- a/tests/signal_handlers/signal_handlers.test.js +++ b/tests/signal_handlers/signal_handlers.test.js @@ -41,7 +41,7 @@ describe("SIGINT handlers", () => { .fuzzEntryPoint("SIGINT_SYNC") .build(); fuzzTest.execute(); - assertSigintMessagesLogged(fuzzTest); + assertSignalMessagesLogged(fuzzTest); }); it("stop async fuzzing on SIGINT", () => { const fuzzTest = fuzzTestBuilder @@ -49,7 +49,7 @@ describe("SIGINT handlers", () => { .fuzzEntryPoint("SIGINT_ASYNC") .build(); fuzzTest.execute(); - assertSigintMessagesLogged(fuzzTest); + assertSignalMessagesLogged(fuzzTest); }); }); @@ -61,7 +61,7 @@ describe("SIGINT handlers", () => { .jestRunInFuzzingMode(true) .build(); fuzzTest.execute(); - assertSigintMessagesLogged(fuzzTest); + assertSignalMessagesLogged(fuzzTest); }); it("stop async fuzzing on SIGINT", () => { const fuzzTest = fuzzTestBuilder @@ -70,13 +70,70 @@ describe("SIGINT handlers", () => { .jestRunInFuzzingMode(true) .build(); fuzzTest.execute(); - assertSigintMessagesLogged(fuzzTest); + assertSignalMessagesLogged(fuzzTest); }); }); }); -function assertSigintMessagesLogged(fuzzTest) { - expect(fuzzTest.stdout).toContain("kill with SIGINT"); +describe("SIGSEGV handlers", () => { + let fuzzTestBuilder; + const errorMessage = "= Segmentation Fault"; + + beforeEach(() => { + fuzzTestBuilder = new FuzzTestBuilder() + .runs(20000) + .dir(path.join(__dirname, "SIGSEGV")) + .coverage(true) + .verbose(true); + }); + + describe("in standalone fuzzing mode", () => { + it("stop sync fuzzing on SIGSEGV", () => { + const fuzzTest = fuzzTestBuilder + .sync(true) + .fuzzEntryPoint("SIGSEGV_SYNC") + .build(); + expect(() => fuzzTest.execute()).toThrowError(); + assertSignalMessagesLogged(fuzzTest); + assertErrorAndCrashFileLogged(fuzzTest, errorMessage); + }); + it("stop async fuzzing on SIGSEGV", () => { + const fuzzTest = fuzzTestBuilder + .sync(false) + .fuzzEntryPoint("SIGSEGV_ASYNC") + .build(); + expect(() => fuzzTest.execute()).toThrowError(); + assertSignalMessagesLogged(fuzzTest); + assertErrorAndCrashFileLogged(fuzzTest, errorMessage); + }); + }); + + describe("in Jest fuzzing mode", () => { + it("stop sync fuzzing on SIGSEGV", () => { + const fuzzTest = fuzzTestBuilder + .jestTestFile("tests.fuzz.js") + .jestTestName("^Jest Sync$") + .jestRunInFuzzingMode(true) + .build(); + expect(() => fuzzTest.execute()).toThrowError(); + assertSignalMessagesLogged(fuzzTest); + assertErrorAndCrashFileLogged(fuzzTest, errorMessage); + }); + it("stop async fuzzing on SIGSEGV", () => { + const fuzzTest = fuzzTestBuilder + .jestTestFile("tests.fuzz.js") + .jestTestName("^Jest Async$") + .jestRunInFuzzingMode(true) + .build(); + expect(() => fuzzTest.execute()).toThrowError(); + assertSignalMessagesLogged(fuzzTest); + assertErrorAndCrashFileLogged(fuzzTest, errorMessage); + }); + }); +}); + +function assertSignalMessagesLogged(fuzzTest) { + expect(fuzzTest.stdout).toContain("kill with signal"); // We asked for a coverage report. Here we only look for the universal part of its header. expect(fuzzTest.stdout).toContain( @@ -85,6 +142,11 @@ function assertSigintMessagesLogged(fuzzTest) { // "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", + "Signal has not stopped the fuzzing process", ); } + +function assertErrorAndCrashFileLogged(fuzzTest, errorMessage) { + expect(fuzzTest.stdout).toContain(errorMessage); + expect(fuzzTest.stderr).toContain("Test unit written to "); +}