Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ node_modules/
*.tgz

# Dictionaries generated by Jazzer.js
.JazzerJs-merged-dictionaries
.JazzerJs-merged-dictionaries

compile_commands.json
3 changes: 1 addition & 2 deletions packages/fuzzer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions packages/fuzzer/fuzzing_async.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

#include "napi.h"
#include <cstdlib>
#include <csignal>
#include <csetjmp>
#include <future>
#include <iostream>

Expand Down Expand Up @@ -66,6 +68,14 @@ using FinalizerDataType = void;

TSFN gTSFN;

const std::string SEGFAULT_ERROR_MESSAGE = "Segmentation fault found in fuzz target";

std::jmp_buf errorBuffer;

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<void *> promise;
Expand Down Expand Up @@ -107,6 +117,10 @@ void CallJsFuzzCallback(Napi::Env env, Napi::Function jsFuzzCallback,
// thread and continue with the next invocation.

try {
if (setjmp(errorBuffer) != 0) {
std::cerr << SEGFAULT_ERROR_MESSAGE << std::endl;
exit(139);
}
if (env != nullptr) {
auto buffer = Napi::Buffer<uint8_t>::Copy(env, data->data, data->size);

Expand Down Expand Up @@ -288,6 +302,7 @@ Napi::Value StartFuzzingAsync(const Napi::CallbackInfo &info) {
context->native_thread = std::thread(
[](std::vector<std::string> fuzzer_args, AsyncFuzzTargetContext *ctx) {
try {
signal(SIGSEGV, ErrorSignalHandler);
StartLibFuzzer(fuzzer_args, FuzzCallbackAsync);
} catch (const JSException &exception) {
}
Expand Down
42 changes: 34 additions & 8 deletions packages/fuzzer/fuzzing_sync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
#include "utils.h"
#include <csignal>
#include <cstdlib>
#include <iostream>
#include <optional>
#include <csetjmp>

namespace {
const std::string SEGFAULT_ERROR_MESSAGE = "Segmentation fault found in fuzz target";

// Information about a JS fuzz target.
struct FuzzTargetInfo {
Napi::Env env;
Expand All @@ -36,9 +40,22 @@ std::optional<FuzzTargetInfo> 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; }
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 error_signal_handler(int signum) {
gSignalStatus = signum;
std::longjmp(errorBuffer, signum);
}

// The libFuzzer callback when fuzzing synchronously
int FuzzCallbackSync(const uint8_t *Data, size_t Size) {
Expand All @@ -62,15 +79,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<uint8_t>::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 `error_signal_handler` for why
if (gSignalStatus == SIGSEGV) {
std::cerr << SEGFAULT_ERROR_MESSAGE << std::endl;
exit(139);
}

// Non-zero exit codes will produce crash files.
auto exitCode = Napi::Number::New(gFuzzTarget->env, 0);

Expand Down Expand Up @@ -111,7 +137,7 @@ void StartFuzzing(const Napi::CallbackInfo &info) {
info[2].As<Napi::Function>()};

signal(SIGINT, sigintHandler);
signal(SIGSEGV, sigintHandler);
signal(SIGSEGV, error_signal_handler);

StartLibFuzzer(fuzzer_args, FuzzCallbackSync);
// Explicitly reset the global function pointer because the JS
Expand Down
21 changes: 21 additions & 0 deletions tests/signal_handlers/SIGSEGV/fuzz.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

const native = require("native-signal");

let i = 0;

module.exports.SIGSEGV_SYNC = (data) => {
Expand All @@ -36,3 +38,22 @@ module.exports.SIGSEGV_ASYNC = (data) => {
}
i++;
};

module.exports.NATIVE_SIGSEGV_SYNC = (data) => {
if (i === 1000) {
console.log("kill with signal");
native.sigsegv(0);
}
if (i > 1000) {
console.log("Signal has not stopped the fuzzing process");
}
i++;
};

module.exports.NATIVE_SIGSEGV_ASYNC = async (data) => {
if (i === 1000) {
console.log("kill with signal");
native.sigsegv(0);
}
i++;
};
3 changes: 2 additions & 1 deletion tests/signal_handlers/SIGSEGV/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
9 changes: 8 additions & 1 deletion tests/signal_handlers/SIGSEGV/tests.fuzz.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
45 changes: 45 additions & 0 deletions tests/signal_handlers/native-signal/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
27 changes: 27 additions & 0 deletions tests/signal_handlers/native-signal/index.ts
Original file line number Diff line number Diff line change
@@ -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);
}
24 changes: 24 additions & 0 deletions tests/signal_handlers/native-signal/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "native-signal",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"install": "npm run build",
"build": "cmake-js build && tsc"
},
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^5.2.2"
},
"binary": {
"napi_versions": [
4
]
},
"dependencies": {
"bindings": "^1.5.0",
"cmake-js": "^7.2.1"
}
}
25 changes: 25 additions & 0 deletions tests/signal_handlers/native-signal/signal_impl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include <iostream>
#include <signal.h>

#include <napi.h>

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<sigsegv>(env);

return exports;
}

NODE_API_MODULE(signal_impl, Init);
Loading