Skip to content

Commit f75fc01

Browse files
oetrbertschneider
authored andcommitted
Add SIGINT handlers for sync and async modes
In async mode, we can register a callback directly in Node. In sync mode, the event loop is blocked until the fuzzing process has finished. Hence, we register the callback for SIGINT directly in our native addon.
1 parent 7664be7 commit f75fc01

File tree

11 files changed

+294
-21
lines changed

11 files changed

+294
-21
lines changed

packages/core/core.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Code Intelligence GmbH
2+
* Copyright 2023 Code Intelligence GmbH
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,7 +14,6 @@
1414
* limitations under the License.
1515
*/
1616
import path from "path";
17-
import * as process from "process";
1817
import * as tmp from "tmp";
1918
import * as fs from "fs";
2019

@@ -169,14 +168,40 @@ export async function startFuzzingNoInit(
169168
fuzzFn: fuzzer.FuzzTarget,
170169
options: Options,
171170
) {
171+
// Signal handler that stops fuzzing when the process receives a SIGINT,
172+
// necessary to generate coverage reports and print debug information.
173+
// The handler stops the process via `stopFuzzing`, as resolving the "fuzzing
174+
// promise" does not work in sync mode due to the blocked event loop.
175+
const signalHandler = () => {
176+
stopFuzzing(
177+
undefined,
178+
options.expectedErrors,
179+
options.coverageDirectory,
180+
options.coverageReporters,
181+
options.sync,
182+
0,
183+
);
184+
};
185+
172186
const fuzzerOptions = buildFuzzerOptions(options);
173187
logInfoAboutFuzzerOptions(fuzzerOptions);
174-
const fuzzerFn = options.sync
175-
? Fuzzer.startFuzzing
176-
: Fuzzer.startFuzzingAsync;
177-
// Wrap the potentially sync fuzzer call, so that resolve and exception
178-
// handlers are always executed.
179-
return Promise.resolve().then(() => fuzzerFn(fuzzFn, fuzzerOptions));
188+
189+
if (options.sync) {
190+
return Promise.resolve().then(() =>
191+
Fuzzer.startFuzzing(
192+
fuzzFn,
193+
fuzzerOptions,
194+
// In synchronous mode, we cannot use the SIGINT handler in Node,
195+
// because it won't be called until the fuzzing process is finished.
196+
// Hence, we pass a callback function to the native fuzzer.
197+
signalHandler,
198+
),
199+
);
200+
} else {
201+
// Add a Node SIGINT handler to stop fuzzing gracefully.
202+
process.on("SIGINT", signalHandler);
203+
return Fuzzer.startFuzzingAsync(fuzzFn, fuzzerOptions);
204+
}
180205
}
181206

182207
function prepareLibFuzzerArg0(fuzzerOptions: string[]): string {
@@ -238,6 +263,7 @@ function stopFuzzing(
238263
coverageDirectory: string,
239264
coverageReporters: reports.ReportType[],
240265
sync: boolean,
266+
forceShutdownWithCode?: number,
241267
) {
242268
const stopFuzzing = sync ? Fuzzer.stopFuzzing : Fuzzer.stopFuzzingAsync;
243269
if (process.env.JAZZER_DEBUG) {
@@ -257,13 +283,15 @@ function stopFuzzing(
257283
);
258284
}
259285

260-
// No error found, check if one is expected.
286+
// No error found, check if one is expected or an exit code should be enforced.
261287
if (!err) {
262288
if (expectedErrors.length) {
263289
console.error(
264290
`ERROR: Received no error, but expected one of [${expectedErrors}].`,
265291
);
266292
stopFuzzing(ERROR_UNEXPECTED_CODE);
293+
} else if (forceShutdownWithCode !== undefined) {
294+
stopFuzzing(forceShutdownWithCode);
267295
}
268296
return;
269297
}

packages/fuzzer/addon.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type FuzzOpts = string[];
2727
export type StartFuzzingSyncFn = (
2828
fuzzFn: FuzzTarget,
2929
fuzzOpts: FuzzOpts,
30+
sigintCallback: () => void,
3031
) => void;
3132
export type StartFuzzingAsyncFn = (
3233
fuzzFn: FuzzTarget,

packages/fuzzer/fuzzing_sync.cpp

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2022 Code Intelligence GmbH
1+
// Copyright 2023 Code Intelligence GmbH
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
1515
#include "fuzzing_sync.h"
1616
#include "shared/libfuzzer.h"
1717
#include "utils.h"
18+
#include <csignal>
1819
#include <cstdlib>
1920
#include <optional>
2021

@@ -23,14 +24,22 @@ namespace {
2324
struct FuzzTargetInfo {
2425
Napi::Env env;
2526
Napi::Function target;
27+
Napi::Function stopFunction;
2628
};
2729

2830
// The JS fuzz target. We need to store the function pointer in a global
2931
// variable because libfuzzer doesn't give us a way to feed user-provided data
3032
// to its target function.
3133
std::optional<FuzzTargetInfo> gFuzzTarget;
34+
35+
// Track if SIGINT signal handler was called.
36+
// This is only necessary in the sync fuzzing case, as async can be handled
37+
// much nicer directly in JavaScript.
38+
volatile std::sig_atomic_t gSignalStatus;
3239
} // namespace
3340

41+
void sigintHandler(int signum) { gSignalStatus = signum; }
42+
3443
// The libFuzzer callback when fuzzing synchronously
3544
int FuzzCallbackSync(const uint8_t *Data, size_t Size) {
3645
// 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) {
6069
} else {
6170
SyncReturnsHandler();
6271
}
72+
73+
// Execute the signal handler in context of the node application.
74+
if (gSignalStatus != 0) {
75+
gFuzzTarget->stopFunction.Call({});
76+
}
77+
6378
return EXIT_SUCCESS;
6479
}
6580

@@ -71,17 +86,24 @@ int FuzzCallbackSync(const uint8_t *Data, size_t Size) {
7186
// parameter; the fuzz target's return value is ignored. The second argument
7287
// is an array of (command-line) arguments to pass to libfuzzer.
7388
void StartFuzzing(const Napi::CallbackInfo &info) {
74-
if (info.Length() != 2 || !info[0].IsFunction() || !info[1].IsArray()) {
75-
throw Napi::Error::New(info.Env(),
76-
"Need two arguments, which must be the fuzz target "
77-
"function and an array of libfuzzer arguments");
89+
if (info.Length() != 3 || !info[0].IsFunction() || !info[1].IsArray() ||
90+
!info[2].IsFunction()) {
91+
throw Napi::Error::New(
92+
info.Env(),
93+
"Need three arguments, which must be the fuzz target "
94+
"function, an array of libfuzzer arguments, and a callback function "
95+
"that the fuzzer will call in case of SIGINT or a segmentation fault");
7896
}
7997

8098
auto fuzzer_args = LibFuzzerArgs(info.Env(), info[1].As<Napi::Array>());
8199

82100
// Store the JS fuzz target and corresponding environment globally, so that
83-
// our C++ fuzz target can use them to call back into JS.
84-
gFuzzTarget = {info.Env(), info[0].As<Napi::Function>()};
101+
// our C++ fuzz target can use them to call back into JS. Also store the stop
102+
// function that will be called in case of a SIGINT.
103+
gFuzzTarget = {info.Env(), info[0].As<Napi::Function>(),
104+
info[2].As<Napi::Function>()};
105+
106+
signal(SIGINT, sigintHandler);
85107

86108
StartLibFuzzer(fuzzer_args, FuzzCallbackSync);
87109
// Explicitly reset the global function pointer because the JS
@@ -93,9 +115,9 @@ void StartFuzzing(const Napi::CallbackInfo &info) {
93115
void StopFuzzing(const Napi::CallbackInfo &info) {
94116
int exitCode = StopFuzzingHandleExit(info);
95117

96-
// If we ran in async mode and we only ever encountered synchronous results
118+
// If we ran in async mode, and we only ever encountered synchronous results
97119
// we'll give an indicator that running in synchronous mode is likely
98-
// benefical
120+
// beneficial.
99121
ReturnValueInfo(true);
100122

101123
// We call _Exit to immediately terminate the process without performing any

packages/fuzzer/utils.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2022 Code Intelligence GmbH
1+
// Copyright 2023 Code Intelligence GmbH
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -17,8 +17,6 @@
1717
#include "shared/libfuzzer.h"
1818
#include <iostream>
1919

20-
#define btoa(x) ((x) ? "true" : "false")
21-
2220
void StartLibFuzzer(const std::vector<std::string> &args,
2321
fuzzer::UserCallback fuzzCallback) {
2422
std::vector<char *> fuzzer_arg_pointers;

tests/helpers.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const assert = require("assert");
2727
// `fuzzEntryPoint` (which would return a "1")
2828
const FuzzingExitCode = "77";
2929
const JestRegressionExitCode = "1";
30+
const WindowsExitCode = "1";
3031

3132
class FuzzTest {
3233
sync;
@@ -323,5 +324,6 @@ function callWithTimeout(fn, timeout) {
323324
module.exports.FuzzTestBuilder = FuzzTestBuilder;
324325
module.exports.FuzzingExitCode = FuzzingExitCode;
325326
module.exports.JestRegressionExitCode = JestRegressionExitCode;
327+
module.exports.WindowsExitCode = WindowsExitCode;
326328
module.exports.makeFnCalledOnce = makeFnCalledOnce;
327329
module.exports.callWithTimeout = callWithTimeout;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tests.fuzz
2+
.jazzerjsrc.json

tests/signal_handlers/SIGINT/fuzz.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2023 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
let i = 0;
18+
19+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
20+
module.exports.SIGINT_SYNC = (data) => {
21+
if (i === 1000) {
22+
console.log("kill with SIGINT");
23+
process.kill(process.pid, "SIGINT");
24+
}
25+
if (i > 1000) {
26+
console.log("SIGINT has not stopped the fuzzing process");
27+
}
28+
i++;
29+
};
30+
31+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
32+
module.exports.SIGINT_ASYNC = (data) => {
33+
// Raising SIGINT in async mode does not stop the fuzzer directly,
34+
// as the event is handled asynchronously in the event loop.
35+
if (i === 1000) {
36+
console.log("kill with SIGINT");
37+
process.kill(process.pid, "SIGINT");
38+
}
39+
i++;
40+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "jazzerjs-signal-handler-tests",
3+
"version": "1.0.0",
4+
"description": "Tests for the SIGINT signal handler",
5+
"scripts": {
6+
"test": "jest",
7+
"fuzz": "JAZZER_FUZZ=1 jest"
8+
},
9+
"devDependencies": {
10+
"@jazzer.js/jest-runner": "file:../../packages/jest-runner"
11+
},
12+
"jest": {
13+
"projects": [
14+
{
15+
"runner": "@jazzer.js/jest-runner",
16+
"displayName": {
17+
"name": "Jazzer.js",
18+
"color": "cyan"
19+
},
20+
"testMatch": [
21+
"<rootDir>/**/*.fuzz.js"
22+
]
23+
}
24+
]
25+
}
26+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2023 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/* eslint no-undef: 0 */
18+
/* eslint-disable @typescript-eslint/no-var-requires */
19+
20+
const { SIGINT_SYNC, SIGINT_ASYNC } = require("./fuzz.js");
21+
22+
describe("Jest", () => {
23+
it.fuzz("Sync", SIGINT_SYNC);
24+
it.fuzz("Async", SIGINT_ASYNC);
25+
});

tests/signal_handlers/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "jazzer-js-tests-for-signal-handlers",
3+
"version": "1.0.0",
4+
"description": "Tests for Jazzer.js' signal handlers",
5+
"scripts": {
6+
"fuzz": "jest --verbose",
7+
"test": "jest --verbose"
8+
},
9+
"devDependencies": {
10+
"@jazzer.js/core": "file:../../packages/core"
11+
}
12+
}

0 commit comments

Comments
 (0)