Skip to content

Commit 50c6993

Browse files
author
434b
committed
Merge branch 'main' into FUZZ-490_enforce_printable_string
2 parents 7244dcc + 3e86576 commit 50c6993

27 files changed

+1268
-616
lines changed

package-lock.json

Lines changed: 190 additions & 123 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jazzer.js",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "Coverage-guided, in-process fuzzing for Node.js",
55
"homepage": "https://github.com/CodeIntelligenceTesting/jazzer.js#readme",
66
"keywords": [
@@ -36,11 +36,11 @@
3636
},
3737
"devDependencies": {
3838
"@types/bindings": "^1.5.1",
39-
"@types/jest": "^29.2.5",
39+
"@types/jest": "^29.2.6",
4040
"@types/node": "^18.11.18",
41-
"@types/yargs": "^17.0.19",
42-
"@typescript-eslint/eslint-plugin": "^5.48.2",
43-
"@typescript-eslint/parser": "^5.48.2",
41+
"@types/yargs": "^17.0.20",
42+
"@typescript-eslint/eslint-plugin": "^5.49.0",
43+
"@typescript-eslint/parser": "^5.49.0",
4444
"eslint": "^8.32.0",
4545
"eslint-config-prettier": "^8.6.0",
4646
"eslint-plugin-jest": "^27.2.1",

packages/core/cli.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ yargs(process.argv.slice(2))
7474
})
7575
.hide("fuzzFunction")
7676

77+
.option("id_sync_file", {
78+
describe:
79+
"File used to sync edge ID generation. " +
80+
"Needed when fuzzing in multi-process modes",
81+
type: "string",
82+
default: undefined,
83+
group: "Fuzzer:",
84+
})
85+
.hide("id_sync_file")
86+
7787
.option("sync", {
7888
describe: "Run the fuzz target synchronously.",
7989
type: "boolean",
@@ -167,6 +177,7 @@ yargs(process.argv.slice(2))
167177
fuzzerOptions: args.corpus.concat(args._),
168178
customHooks: args.custom_hooks.map(ensureFilepath),
169179
expectedErrors: args.expected_errors,
180+
idSyncFile: args.id_sync_file,
170181
});
171182
}
172183
)

packages/core/core.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,22 @@
1515
*/
1616

1717
import path from "path";
18+
import * as process from "process";
19+
import * as tmp from "tmp";
20+
import * as fs from "fs";
21+
1822
import * as fuzzer from "@jazzer.js/fuzzer";
1923
import * as hooking from "@jazzer.js/hooking";
20-
import { registerInstrumentor } from "@jazzer.js/instrumentor";
2124
import { trackedHooks } from "@jazzer.js/hooking";
25+
import {
26+
registerInstrumentor,
27+
Instrumentor,
28+
FileSyncIdStrategy,
29+
MemorySyncIdStrategy,
30+
} from "@jazzer.js/instrumentor";
31+
32+
// Remove temporary files on exit
33+
tmp.setGracefulCleanup();
2234

2335
// libFuzzer uses exit code 77 in case of a crash, so use a similar one for
2436
// failed error expectations.
@@ -38,6 +50,7 @@ export interface Options {
3850
customHooks: string[];
3951
expectedErrors: string[];
4052
timeout?: number;
53+
idSyncFile?: string;
4154
}
4255

4356
interface FuzzModule {
@@ -54,7 +67,16 @@ export async function initFuzzing(options: Options) {
5467
registerGlobals();
5568
await Promise.all(options.customHooks.map(importModule));
5669
if (!options.dryRun) {
57-
registerInstrumentor(options.includes, options.excludes);
70+
registerInstrumentor(
71+
new Instrumentor(
72+
options.includes,
73+
options.excludes,
74+
75+
options.idSyncFile !== undefined
76+
? new FileSyncIdStrategy(options.idSyncFile)
77+
: new MemorySyncIdStrategy()
78+
)
79+
);
5880
}
5981
}
6082

@@ -90,6 +112,59 @@ export async function startFuzzingNoInit(
90112
return Promise.resolve().then(() => fuzzerFn(fuzzFn, fuzzerOptions));
91113
}
92114

115+
function prepareLibFuzzerArg0(fuzzerOptions: string[]): string {
116+
// When we run in a libFuzzer mode that spawns subprocesses, we create a wrapper script
117+
// that can be used as libFuzzer's argv[0]. In the fork mode, the main libFuzzer process
118+
// uses argv[0] to spawn further processes that perform the actual fuzzing.
119+
const libFuzzerSpawnsProcess = fuzzerOptions.some(
120+
(flag) =>
121+
flag.startsWith("-fork=") ||
122+
flag.startsWith("-jobs=") ||
123+
flag.startsWith("-merge=")
124+
);
125+
126+
if (!libFuzzerSpawnsProcess) {
127+
// Return a fake argv[0] to start the fuzzer if libFuzzer does not spawn new processes.
128+
return "unused_arg0_report_a_bug_if_you_see_this";
129+
} else {
130+
// Create a wrapper script and return its path.
131+
return createWrapperScript(fuzzerOptions);
132+
}
133+
}
134+
135+
function createWrapperScript(fuzzerOptions: string[]) {
136+
const jazzerArgs = process.argv.filter(
137+
(arg) => arg !== "--" && fuzzerOptions.indexOf(arg) === -1
138+
);
139+
140+
if (jazzerArgs.indexOf("--id_sync_file") === -1) {
141+
const idSyncFile = tmp.fileSync({
142+
mode: 0o600,
143+
prefix: "jazzer.js",
144+
postfix: "idSync",
145+
});
146+
jazzerArgs.push("--id_sync_file", idSyncFile.name);
147+
fs.closeSync(idSyncFile.fd);
148+
}
149+
150+
const isWindows = process.platform === "win32";
151+
152+
const scriptContent = `${isWindows ? "@echo off" : "#!/usr/bin/env sh"}
153+
cd "${process.cwd()}"
154+
${jazzerArgs.map((s) => '"' + s + '"').join(" ")} -- ${isWindows ? "%*" : "$@"}
155+
`;
156+
157+
const scriptTempFile = tmp.fileSync({
158+
mode: 0o700,
159+
prefix: "jazzer.js",
160+
postfix: "libfuzzer" + (isWindows ? ".bat" : ".sh"),
161+
});
162+
fs.writeFileSync(scriptTempFile.name, scriptContent);
163+
fs.closeSync(scriptTempFile.fd);
164+
165+
return scriptTempFile.name;
166+
}
167+
93168
function stopFuzzing(err: unknown, expectedErrors: string[]) {
94169
if (process.env.JAZZER_DEBUG) {
95170
trackedHooks.categorizeUnknown(HookManager.hooks).print();
@@ -183,7 +258,8 @@ function buildFuzzerOptions(options: Options): string[] {
183258
const inSeconds = options.timeout / 1000;
184259
opts = opts.concat(`-timeout=${inSeconds}`);
185260
}
186-
return opts;
261+
262+
return [prepareLibFuzzerArg0(opts), ...opts];
187263
}
188264

189265
async function loadFuzzFunction(options: Options): Promise<fuzzer.FuzzTarget> {

packages/core/jazzer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { addon } from "@jazzer.js/fuzzer";
18+
import { fuzzer } from "@jazzer.js/fuzzer";
1919

2020
/**
2121
* Instructs the fuzzer to guide its mutations towards making `current` equal to `target`
@@ -29,7 +29,7 @@ import { addon } from "@jazzer.js/fuzzer";
2929
* @param id a (probabilistically) unique identifier for this particular compare hint
3030
*/
3131
function guideTowardsEquality(current: string, target: string, id: number) {
32-
addon.traceUnequalStrings(id, current, target);
32+
fuzzer.tracer.traceUnequalStrings(id, current, target);
3333
}
3434

3535
/**
@@ -45,7 +45,7 @@ function guideTowardsEquality(current: string, target: string, id: number) {
4545
* @param id a (probabilistically) unique identifier for this particular compare hint
4646
*/
4747
function guideTowardsContainment(needle: string, haystack: string, id: number) {
48-
addon.traceStringContainment(id, needle, haystack);
48+
fuzzer.tracer.traceStringContainment(id, needle, haystack);
4949
}
5050

5151
/**
@@ -63,7 +63,7 @@ function guideTowardsContainment(needle: string, haystack: string, id: number) {
6363
* @param id a (probabilistically) unique identifier for this particular state hint
6464
*/
6565
function exploreState(state: number, id: number) {
66-
addon.tracePcIndir(id, state);
66+
fuzzer.tracer.tracePcIndir(id, state);
6767
}
6868

6969
export interface Jazzer {

packages/core/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@jazzer.js/core",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "Jazzer.js CLI",
55
"homepage": "https://github.com/CodeIntelligenceTesting/jazzer.js#readme",
66
"author": "Code Intelligence",
@@ -21,10 +21,11 @@
2121
"dependencies": {
2222
"@jazzer.js/hooking": "*",
2323
"@jazzer.js/instrumentor": "*",
24+
"tmp": "^0.2.1",
2425
"yargs": "^17.6.2"
2526
},
2627
"devDependencies": {
27-
"@types/yargs": "^17.0.19"
28+
"@types/yargs": "^17.0.20"
2829
},
2930
"engines": {
3031
"node": ">= 14.0.0",

packages/fuzzer/addon.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
import { default as bind } from "bindings";
18+
19+
export type FuzzTargetAsyncOrValue = (data: Buffer) => void | Promise<void>;
20+
export type FuzzTargetCallback = (
21+
data: Buffer,
22+
done: (e?: Error) => void
23+
) => void;
24+
export type FuzzTarget = FuzzTargetAsyncOrValue | FuzzTargetCallback;
25+
export type FuzzOpts = string[];
26+
27+
export type StartFuzzingSyncFn = (
28+
fuzzFn: FuzzTarget,
29+
fuzzOpts: FuzzOpts
30+
) => void;
31+
export type StartFuzzingAsyncFn = (
32+
fuzzFn: FuzzTarget,
33+
fuzzOpts: FuzzOpts
34+
) => Promise<void>;
35+
36+
type NativeAddon = {
37+
registerCoverageMap: (buffer: Buffer) => void;
38+
registerNewCounters: (oldNumCounters: number, newNumCounters: number) => void;
39+
40+
traceUnequalStrings: (
41+
hookId: number,
42+
current: string,
43+
target: string
44+
) => void;
45+
46+
traceStringContainment: (
47+
hookId: number,
48+
needle: string,
49+
haystack: string
50+
) => void;
51+
traceIntegerCompare: (
52+
hookId: number,
53+
current: number,
54+
target: number
55+
) => void;
56+
57+
tracePcIndir: (hookId: number, state: number) => void;
58+
59+
printVersion: () => void;
60+
startFuzzing: StartFuzzingSyncFn;
61+
startFuzzingAsync: StartFuzzingAsyncFn;
62+
stopFuzzingAsync: (status?: number) => void;
63+
};
64+
65+
export const addon: NativeAddon = bind("jazzerjs");

packages/fuzzer/coverage.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
import { addon } from "./addon";
18+
19+
export class CoverageTracker {
20+
private static readonly MAX_NUM_COUNTERS: number = 1 << 20;
21+
private static readonly INITIAL_NUM_COUNTERS: number = 1 << 9;
22+
private readonly coverageMap: Buffer;
23+
private currentNumCounters: number;
24+
25+
constructor() {
26+
this.coverageMap = Buffer.alloc(CoverageTracker.MAX_NUM_COUNTERS, 0);
27+
this.currentNumCounters = CoverageTracker.INITIAL_NUM_COUNTERS;
28+
addon.registerCoverageMap(this.coverageMap);
29+
addon.registerNewCounters(0, this.currentNumCounters);
30+
}
31+
32+
enlargeCountersBufferIfNeeded(nextEdgeId: number) {
33+
// Enlarge registered counters if needed
34+
let newNumCounters = this.currentNumCounters;
35+
while (nextEdgeId >= newNumCounters) {
36+
newNumCounters = 2 * newNumCounters;
37+
if (newNumCounters > CoverageTracker.MAX_NUM_COUNTERS) {
38+
throw new Error(
39+
`Maximum number (${CoverageTracker.MAX_NUM_COUNTERS}) of coverage counts exceeded.`
40+
);
41+
}
42+
}
43+
44+
// Register new counters if enlarged
45+
if (newNumCounters > this.currentNumCounters) {
46+
addon.registerNewCounters(this.currentNumCounters, newNumCounters);
47+
this.currentNumCounters = newNumCounters;
48+
console.log(
49+
`INFO: New number of coverage counters ${this.currentNumCounters}`
50+
);
51+
}
52+
}
53+
54+
/**
55+
* Increments the coverage counter for a given ID.
56+
* This function implements the NeverZero policy from AFL++.
57+
* See https://aflplus.plus//papers/aflpp-woot2020.pdf
58+
* @param edgeId the edge ID of the coverage counter to increment
59+
*/
60+
incrementCounter(edgeId: number) {
61+
const counter = this.coverageMap.readUint8(edgeId);
62+
this.coverageMap.writeUint8(counter == 255 ? 1 : counter + 1, edgeId);
63+
}
64+
65+
readCounter(edgeId: number): number {
66+
return this.coverageMap.readUint8(edgeId);
67+
}
68+
}
69+
70+
export const coverageTracker = new CoverageTracker();

packages/fuzzer/fuzzer.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,26 @@ import { fuzzer } from "./fuzzer";
1919

2020
describe("compare hooks", () => {
2121
it("traceStrCmp supports equals operators", () => {
22-
expect(fuzzer.traceStrCmp("a", "b", "==", 0)).toBe(false);
23-
expect(fuzzer.traceStrCmp("a", "b", "===", 0)).toBe(false);
24-
expect(fuzzer.traceStrCmp("a", "b", "!=", 0)).toBe(true);
25-
expect(fuzzer.traceStrCmp("a", "b", "!==", 0)).toBe(true);
22+
expect(fuzzer.tracer.traceStrCmp("a", "b", "==", 0)).toBe(false);
23+
expect(fuzzer.tracer.traceStrCmp("a", "b", "===", 0)).toBe(false);
24+
expect(fuzzer.tracer.traceStrCmp("a", "b", "!=", 0)).toBe(true);
25+
expect(fuzzer.tracer.traceStrCmp("a", "b", "!==", 0)).toBe(true);
2626
});
2727
});
2828

2929
describe("incrementCounter", () => {
3030
it("should support the NeverZero policy", () => {
31-
expect(fuzzer.readCounter(0)).toBe(0);
31+
expect(fuzzer.coverageTracker.readCounter(0)).toBe(0);
3232
for (let counter = 1; counter <= 512; counter++) {
33-
fuzzer.incrementCounter(0);
33+
fuzzer.coverageTracker.incrementCounter(0);
3434
if (counter < 256) {
35-
expect(fuzzer.readCounter(0)).toBe(counter);
35+
expect(fuzzer.coverageTracker.readCounter(0)).toBe(counter);
3636
} else if (counter < 511) {
37-
expect(fuzzer.readCounter(0)).toBe((counter % 256) + 1);
37+
expect(fuzzer.coverageTracker.readCounter(0)).toBe((counter % 256) + 1);
3838
} else if (counter == 511) {
39-
expect(fuzzer.readCounter(0)).toBe(1);
39+
expect(fuzzer.coverageTracker.readCounter(0)).toBe(1);
4040
} else {
41-
expect(fuzzer.readCounter(0)).toBe((counter % 256) + 2);
41+
expect(fuzzer.coverageTracker.readCounter(0)).toBe((counter % 256) + 2);
4242
}
4343
}
4444
});

0 commit comments

Comments
 (0)