Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
51 changes: 42 additions & 9 deletions packages/core/core.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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";

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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];
}

Expand Down
1 change: 1 addition & 0 deletions packages/fuzzer/addon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type FuzzOpts = string[];
export type StartFuzzingSyncFn = (
fuzzFn: FuzzTarget,
fuzzOpts: FuzzOpts,
sigintCallback: () => void,
) => void;
export type StartFuzzingAsyncFn = (
fuzzFn: FuzzTarget,
Expand Down
40 changes: 31 additions & 9 deletions packages/fuzzer/fuzzing_sync.cpp
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -15,6 +15,7 @@
#include "fuzzing_sync.h"
#include "shared/libfuzzer.h"
#include "utils.h"
#include <csignal>
#include <cstdlib>
#include <optional>

Expand All @@ -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<FuzzTargetInfo> 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
Expand Down Expand Up @@ -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;
}

Expand All @@ -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<Napi::Array>());

// 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<Napi::Function>()};
// 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<Napi::Function>(),
info[2].As<Napi::Function>()};

signal(SIGINT, sigintHandler);

StartLibFuzzer(fuzzer_args, FuzzCallbackSync);
// Explicitly reset the global function pointer because the JS
Expand All @@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/fuzzer/utils.cpp
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,8 +17,6 @@
#include "shared/libfuzzer.h"
#include <iostream>

#define btoa(x) ((x) ? "true" : "false")

void StartLibFuzzer(const std::vector<std::string> &args,
fuzzer::UserCallback fuzzCallback) {
std::vector<char *> fuzzer_arg_pointers;
Expand Down
2 changes: 1 addition & 1 deletion tests/bug-detectors/command-injection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/bug-detectors/general.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/bug-detectors/general/fuzz.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion tests/bug-detectors/path-traversal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/bug-detectors/path-traversal/fuzz.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/";
Expand Down
28 changes: 26 additions & 2 deletions tests/bug-detectors/helpers.js → tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,6 +41,7 @@ class FuzzTest {
jestTestFile;
jestTestNamePattern;
jestRunInFuzzingMode;
coverage;

constructor(
sync,
Expand All @@ -53,6 +55,7 @@ class FuzzTest {
jestTestFile,
jestTestName,
jestRunInFuzzingMode,
coverage,
) {
this.sync = sync;
this.runs = runs;
Expand All @@ -65,6 +68,7 @@ class FuzzTest {
this.jestTestFile = jestTestFile;
this.jestTestNamePattern = jestTestName;
this.jestRunInFuzzingMode = jestRunInFuzzingMode;
this.coverage = coverage;
}

execute() {
Expand All @@ -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);
Expand All @@ -101,6 +106,7 @@ class FuzzTest {
const cmd = "npx";
const options = [
"jest",
this.coverage ? "--coverage" : "",
this.jestTestFile,
'--testNamePattern="' + this.jestTestNamePattern + '"',
];
Expand Down Expand Up @@ -145,6 +151,7 @@ class FuzzTestBuilder {
_jestTestFile = "";
_jestTestName = "";
_jestRunInFuzzingMode = false;
_coverage = false;

/**
* @param {boolean} sync - whether to run the fuzz test in synchronous mode.
Expand Down Expand Up @@ -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.");
Expand All @@ -262,6 +274,7 @@ class FuzzTestBuilder {
this._jestTestFile,
this._jestTestName,
this._jestRunInFuzzingMode,
this._coverage,
);
}
}
Expand Down Expand Up @@ -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;
2 changes: 2 additions & 0 deletions tests/signal_handlers/SIGINT/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tests.fuzz
.jazzerjsrc.json
Loading