diff --git a/.gitignore b/.gitignore index b3295948..6bdd79e7 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,4 @@ node_modules/ *.tgz # Dictionaries generated by Jazzer.js -.JazzerJs-merged-dictionaries \ No newline at end of file +.JazzerJs-merged-dictionaries diff --git a/package.json b/package.json index dd757e7b..31d3200f 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "packages/*" ], "lint-staged": { - "**/*": "prettier --write --ignore-unknown --allow-empty --loglevel debug" + "**/!(compile_commands.json)*": "prettier --write --ignore-unknown --allow-empty --log-level debug" }, "engines": { "node": ">= 14.0.0", diff --git a/packages/fuzzer/CMakeLists.txt b/packages/fuzzer/CMakeLists.txt index e791c7ff..ee87fb53 100644 --- a/packages/fuzzer/CMakeLists.txt +++ b/packages/fuzzer/CMakeLists.txt @@ -154,8 +154,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") ${BINARY_DIR}/${LIBFUZZER_STATIC_LIB_PATH} -Wl,-no-whole-archive) elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") target_link_libraries( - ${PROJECT_NAME} -Wl,-all_load ${BINARY_DIR}/${LIBFUZZER_STATIC_LIB_PATH} - -Wl,-noall_load) + ${PROJECT_NAME} -Wl,-all_load ${BINARY_DIR}/${LIBFUZZER_STATIC_LIB_PATH}) elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") # Force MSVC to do an MT build, suggested by cmake-js cmake_policy(SET CMP0091 NEW) diff --git a/packages/fuzzer/fuzzing_async.cpp b/packages/fuzzer/fuzzing_async.cpp index 6b3cbbd2..1d7e1780 100644 --- a/packages/fuzzer/fuzzing_async.cpp +++ b/packages/fuzzer/fuzzing_async.cpp @@ -13,6 +13,8 @@ // limitations under the License. #include "napi.h" +#include +#include #include #include #include @@ -66,6 +68,15 @@ using FinalizerDataType = void; TSFN gTSFN; +const std::string SEGFAULT_ERROR_MESSAGE = + "Segmentation fault found in fuzz target"; + +std::jmp_buf errorBuffer; + +// See comment on `ErrorSignalHandler` in `fuzzing_sync.cpp` for what this is +// for +void ErrorSignalHandler(int signum) { std::longjmp(errorBuffer, signum); } + // The libFuzzer callback when fuzzing asynchronously. int FuzzCallbackAsync(const uint8_t *Data, size_t Size) { std::promise promise; @@ -107,6 +118,14 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function jsFuzzCallback, // thread and continue with the next invocation. try { + // Return point for the segfault error handler + // This MUST BE called from the thread that executes the fuzz target (and + // thus is the thread with the segfault) otherwise longjmp's behavior is + // undefined + if (setjmp(errorBuffer) != 0) { + std::cerr << SEGFAULT_ERROR_MESSAGE << std::endl; + exit(EXIT_FAILURE); + } if (env != nullptr) { auto buffer = Napi::Buffer::Copy(env, data->data, data->size); @@ -288,6 +307,7 @@ Napi::Value StartFuzzingAsync(const Napi::CallbackInfo &info) { context->native_thread = std::thread( [](std::vector fuzzer_args, AsyncFuzzTargetContext *ctx) { try { + signal(SIGSEGV, ErrorSignalHandler); StartLibFuzzer(fuzzer_args, FuzzCallbackAsync); } catch (const JSException &exception) { } diff --git a/packages/fuzzer/fuzzing_sync.cpp b/packages/fuzzer/fuzzing_sync.cpp index 69d7a727..41848f5c 100644 --- a/packages/fuzzer/fuzzing_sync.cpp +++ b/packages/fuzzer/fuzzing_sync.cpp @@ -15,11 +15,16 @@ #include "fuzzing_sync.h" #include "shared/libfuzzer.h" #include "utils.h" +#include #include #include +#include #include namespace { +const std::string SEGFAULT_ERROR_MESSAGE = + "Segmentation fault found in fuzz target"; + // Information about a JS fuzz target. struct FuzzTargetInfo { Napi::Env env; @@ -36,10 +41,24 @@ std::optional gFuzzTarget; // 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; +std::jmp_buf errorBuffer; } // namespace void sigintHandler(int signum) { gSignalStatus = signum; } +// This handles signals that indicate an unrecoverable error (currently only +// segfaults). Our handling of segfaults is odd because it avoids using our +// Javascript method to print and instead prints a message within C++ and exits +// almost immediately. This is because Node seems to really not like being +// called back into after `longjmp` jumps outside the scope Node thinks it +// should be in and so things in JS-land get pretty broken. However, catching it +// here, printing an ok error message, and letting libfuzzer make the crash file +// is good enough +void ErrorSignalHandler(int signum) { + gSignalStatus = signum; + std::longjmp(errorBuffer, 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 @@ -62,15 +81,24 @@ int FuzzCallbackSync(const uint8_t *Data, size_t Size) { // nice for efficiency if we could use a pointer instead of copying. // auto data = Napi::Buffer::Copy(gFuzzTarget->env, Data, Size); - auto result = gFuzzTarget->target.Call({data}); - - if (result.IsPromise()) { - AsyncReturnsHandler(); - } else { - SyncReturnsHandler(); + if (setjmp(errorBuffer) == 0) { + auto result = gFuzzTarget->target.Call({data}); + if (result.IsPromise()) { + AsyncReturnsHandler(); + } else { + SyncReturnsHandler(); + } } if (gSignalStatus != 0) { + // if we caught a segfault, print the error message and die, letting + // libfuzzer print the crash file. See the comment on `ErrorSignalHandler` + // for why + if (gSignalStatus == SIGSEGV) { + std::cerr << SEGFAULT_ERROR_MESSAGE << std::endl; + exit(EXIT_FAILURE); + } + // Non-zero exit codes will produce crash files. auto exitCode = Napi::Number::New(gFuzzTarget->env, 0); @@ -111,7 +139,7 @@ void StartFuzzing(const Napi::CallbackInfo &info) { info[2].As()}; signal(SIGINT, sigintHandler); - signal(SIGSEGV, sigintHandler); + signal(SIGSEGV, ErrorSignalHandler); StartLibFuzzer(fuzzer_args, FuzzCallbackSync); // Explicitly reset the global function pointer because the JS diff --git a/tests/signal_handlers/SIGSEGV/fuzz.js b/tests/signal_handlers/SIGSEGV/fuzz.js index b6ac855a..b85077af 100644 --- a/tests/signal_handlers/SIGSEGV/fuzz.js +++ b/tests/signal_handlers/SIGSEGV/fuzz.js @@ -14,14 +14,18 @@ * limitations under the License. */ +const native = require("native-signal"); + +const RUN_ON_ITERATION = 1000; + let i = 0; module.exports.SIGSEGV_SYNC = (data) => { - if (i === 1000) { + if (i === RUN_ON_ITERATION) { console.log("kill with signal"); process.kill(process.pid, "SIGSEGV"); } - if (i > 1000) { + if (i > RUN_ON_ITERATION) { console.log("Signal has not stopped the fuzzing process"); } i++; @@ -30,9 +34,26 @@ module.exports.SIGSEGV_SYNC = (data) => { 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) { + if (i === RUN_ON_ITERATION) { console.log("kill with signal"); process.kill(process.pid, "SIGSEGV"); } i++; }; + +module.exports.NATIVE_SIGSEGV_SYNC = (data) => { + if (i === RUN_ON_ITERATION) { + native.sigsegv(0); + } + if (i > RUN_ON_ITERATION) { + console.log("Signal has not stopped the fuzzing process"); + } + i++; +}; + +module.exports.NATIVE_SIGSEGV_ASYNC = async (data) => { + if (i === RUN_ON_ITERATION) { + native.sigsegv(0); + } + i++; +}; diff --git a/tests/signal_handlers/SIGSEGV/package.json b/tests/signal_handlers/SIGSEGV/package.json index 0c3522d2..721f982b 100644 --- a/tests/signal_handlers/SIGSEGV/package.json +++ b/tests/signal_handlers/SIGSEGV/package.json @@ -7,7 +7,8 @@ "fuzz": "JAZZER_FUZZ=1 jest" }, "devDependencies": { - "@jazzer.js/jest-runner": "file:../../packages/jest-runner" + "@jazzer.js/jest-runner": "file:../../packages/jest-runner", + "native-signal": "file:../native-signal" }, "jest": { "projects": [ diff --git a/tests/signal_handlers/SIGSEGV/tests.fuzz.js b/tests/signal_handlers/SIGSEGV/tests.fuzz.js index 8525e712..8072c3bc 100644 --- a/tests/signal_handlers/SIGSEGV/tests.fuzz.js +++ b/tests/signal_handlers/SIGSEGV/tests.fuzz.js @@ -14,9 +14,16 @@ * limitations under the License. */ -const { SIGSEGV_ASYNC, SIGSEGV_SYNC } = require("./fuzz.js"); +const { + SIGSEGV_ASYNC, + SIGSEGV_SYNC, + NATIVE_SIGSEGV_SYNC, + NATIVE_SIGSEGV_ASYNC, +} = require("./fuzz.js"); describe("Jest", () => { it.fuzz("Sync", SIGSEGV_SYNC); it.fuzz("Async", SIGSEGV_ASYNC); + it.fuzz("Native", NATIVE_SIGSEGV_SYNC); + it.fuzz("Native Async", NATIVE_SIGSEGV_ASYNC); }); diff --git a/tests/signal_handlers/native-signal/CMakeLists.txt b/tests/signal_handlers/native-signal/CMakeLists.txt new file mode 100644 index 00000000..da11b2c8 --- /dev/null +++ b/tests/signal_handlers/native-signal/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.15) +cmake_policy(SET CMP0091 NEW) +cmake_policy(SET CMP0042 NEW) + +project(signal_impl) + +set(CMAKE_CXX_STANDARD 17) # mostly supported since GCC 7 +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(LLVM_ENABLE_LLD TRUE) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24: +if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") + cmake_policy(SET CMP0135 NEW) +endif() + +# To help with development, let's write compile_commands.json unconditionally. +set(CMAKE_EXPORT_COMPILE_COMMANDS 1) + +include_directories(${CMAKE_JS_INC}) + +file(GLOB SOURCE_FILES "*.cpp") + +add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC}) +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") +target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB}) + +if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) + # Generate node.lib + execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) +endif() + +# Enable the functionality of Node-API version 4 and disable everything added +# later, so that we don't accidentally break compatibility with older versions +# of Node (see https://nodejs.org/api/n-api.html#node-api-version-matrix). +# +# Note that prebuild recommends in its README to use ${napi_build_version} here, +# but the variable is only set when cmake-js is invoked via prebuild (in which +# case the API version is taken from "binary.napi_versions" in package.json). +# Since we want the build to work in other cases as well, let's just use a +# constant. (There is currently no point in a dynamic setting anyway since we +# specify the oldest version that we're compatible with, and Node-API's ABI +# stability guarantees that this version is available in all future Node-API +# releases.) +add_definitions(-DNAPI_VERSION=4) diff --git a/tests/signal_handlers/native-signal/compile_commands.json b/tests/signal_handlers/native-signal/compile_commands.json new file mode 120000 index 00000000..25eb4b2b --- /dev/null +++ b/tests/signal_handlers/native-signal/compile_commands.json @@ -0,0 +1 @@ +build/compile_commands.json \ No newline at end of file diff --git a/tests/signal_handlers/native-signal/index.ts b/tests/signal_handlers/native-signal/index.ts new file mode 100644 index 00000000..36134352 --- /dev/null +++ b/tests/signal_handlers/native-signal/index.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +import { default as bind } from "bindings"; + +type NativeAddon = { + sigsegv: (loc: number) => void; +}; + +const addon: NativeAddon = bind("signal_impl"); + +export function sigsegv(loc: number) { + addon.sigsegv(loc); +} diff --git a/tests/signal_handlers/native-signal/package.json b/tests/signal_handlers/native-signal/package.json new file mode 100644 index 00000000..49195b69 --- /dev/null +++ b/tests/signal_handlers/native-signal/package.json @@ -0,0 +1,23 @@ +{ + "name": "native-signal", + "version": "1.0.0", + "main": "dist/index.js", + "scripts": { + "postinstall": "npm run build", + "build": "cmake-js build && tsc", + "format:fix": "clang-format -i *.cpp" + }, + "devDependencies": { + "typescript": "^5.2.2", + "clang-format": "^1.8.0" + }, + "binary": { + "napi_versions": [ + 4 + ] + }, + "dependencies": { + "bindings": "^1.5.0", + "cmake-js": "^7.2.1" + } +} diff --git a/tests/signal_handlers/native-signal/signal_impl.cpp b/tests/signal_handlers/native-signal/signal_impl.cpp new file mode 100644 index 00000000..255e6985 --- /dev/null +++ b/tests/signal_handlers/native-signal/signal_impl.cpp @@ -0,0 +1,37 @@ +// 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. + +#include +#include + +#include + +void sigsegv(const Napi::CallbackInfo &info) { + if (info.Length() != 1 || !info[0].IsNumber()) { + throw Napi::Error::New(info.Env(), "Need a single integer argument"); + } + // accepts a parameter to prevent the compiler from optimizing a static + // segfault away + int location = info[0].ToNumber(); + int *a = (int *)location; + *a = 10; +} + +Napi::Object Init(Napi::Env env, Napi::Object exports) { + exports["sigsegv"] = Napi::Function::New(env); + + return exports; +} + +NODE_API_MODULE(signal_impl, Init); diff --git a/tests/signal_handlers/native-signal/tsconfig.json b/tests/signal_handlers/native-signal/tsconfig.json new file mode 100644 index 00000000..d02ef81c --- /dev/null +++ b/tests/signal_handlers/native-signal/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "module": "commonjs" /* Specify what module code is generated. */, + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + "strict": true /* Enable all strict type-checking options. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "exclude": ["build", "dist"] +} diff --git a/tests/signal_handlers/package.json b/tests/signal_handlers/package.json index ac9507a5..175723bd 100644 --- a/tests/signal_handlers/package.json +++ b/tests/signal_handlers/package.json @@ -3,10 +3,12 @@ "version": "1.0.0", "description": "Tests for Jazzer.js' signal handlers", "scripts": { + "install": "cd native-signal && npm install", "fuzz": "jest --verbose", "test": "jest --verbose" }, "devDependencies": { - "@jazzer.js/core": "file:../../packages/core" + "@jazzer.js/core": "file:../../packages/core", + "native-signal": "file:native-signal" } } diff --git a/tests/signal_handlers/signal_handlers.test.js b/tests/signal_handlers/signal_handlers.test.js index 7e98d01b..e110e4de 100644 --- a/tests/signal_handlers/signal_handlers.test.js +++ b/tests/signal_handlers/signal_handlers.test.js @@ -76,7 +76,7 @@ describe("SIGINT handlers", () => { describe("SIGSEGV handlers", () => { let fuzzTestBuilder; - const errorMessage = "= Segmentation Fault"; + const errorMessage = "Segmentation fault found in fuzz target"; beforeEach(() => { fuzzTestBuilder = new FuzzTestBuilder() @@ -93,6 +93,7 @@ describe("SIGSEGV handlers", () => { .build(); expect(() => fuzzTest.execute()).toThrowError(); assertSignalMessagesLogged(fuzzTest); + assertFuzzingStopped(fuzzTest); assertErrorAndCrashFileLogged(fuzzTest, errorMessage); }); it("stop async fuzzing on SIGSEGV", () => { @@ -102,6 +103,25 @@ describe("SIGSEGV handlers", () => { .build(); expect(() => fuzzTest.execute()).toThrowError(); assertSignalMessagesLogged(fuzzTest); + assertFuzzingStopped(fuzzTest); + assertErrorAndCrashFileLogged(fuzzTest, errorMessage); + }); + it("stop fuzzing on native SIGSEGV", () => { + const fuzzTest = fuzzTestBuilder + .sync(true) + .fuzzEntryPoint("NATIVE_SIGSEGV_SYNC") + .build(); + expect(() => fuzzTest.execute()).toThrowError(); + assertFuzzingStopped(fuzzTest); + assertErrorAndCrashFileLogged(fuzzTest, errorMessage); + }); + it("stop fuzzing on native async SIGSEGV", () => { + const fuzzTest = fuzzTestBuilder + .sync(false) + .fuzzEntryPoint("NATIVE_SIGSEGV_ASYNC") + .build(); + expect(() => fuzzTest.execute()).toThrowError(); + assertFuzzingStopped(fuzzTest); assertErrorAndCrashFileLogged(fuzzTest, errorMessage); }); }); @@ -115,6 +135,7 @@ describe("SIGSEGV handlers", () => { .build(); expect(() => fuzzTest.execute()).toThrowError(); assertSignalMessagesLogged(fuzzTest); + assertFuzzingStopped(fuzzTest); assertErrorAndCrashFileLogged(fuzzTest, errorMessage); }); it("stop async fuzzing on SIGSEGV", () => { @@ -125,6 +146,29 @@ describe("SIGSEGV handlers", () => { .build(); expect(() => fuzzTest.execute()).toThrowError(); assertSignalMessagesLogged(fuzzTest); + assertFuzzingStopped(fuzzTest); + assertErrorAndCrashFileLogged(fuzzTest, errorMessage); + }); + it("stop sync fuzzing on native SIGSEGV", () => { + const fuzzTest = fuzzTestBuilder + .jestTestFile("tests.fuzz.js") + .jestTestName("^Jest Native$") + .jestRunInFuzzingMode(true) + .verbose(true) + .build(); + expect(() => fuzzTest.execute()).toThrowError(); + assertFuzzingStopped(fuzzTest); + assertErrorAndCrashFileLogged(fuzzTest, errorMessage); + }); + it("stop async fuzzing on native SIGSEGV", () => { + const fuzzTest = fuzzTestBuilder + .jestTestFile("tests.fuzz.js") + .jestTestName("^Jest Native Async$") + .jestRunInFuzzingMode(true) + .verbose(true) + .build(); + expect(() => fuzzTest.execute()).toThrowError(); + assertFuzzingStopped(fuzzTest); assertErrorAndCrashFileLogged(fuzzTest, errorMessage); }); }); @@ -132,13 +176,9 @@ describe("SIGSEGV handlers", () => { 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. - // Jest prints to stdout. - expect(fuzzTest.stdout).toContain( - "| % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s", - ); - +function assertFuzzingStopped(fuzzTest) { // Count how many times "Signal has not stopped the fuzzing process" has been printed. const matches = fuzzTest.stdout.match( /Signal has not stopped the fuzzing process/g,