Skip to content

Commit 6cdc9ef

Browse files
committed
test: mixed CJS+ESM fuzzing and compareHooks unit tests
Add an integration test where the fuzz target imports both a CJS module (hookRequire path) and an ESM module (loader hook path). Each function compares against a 10-byte random string literal that can only be discovered through compare hooks feeding dictionary entries to libFuzzer from both instrumentation paths. Also add unit tests for esmCodeCoverage combined with compareHooks (string, number, variable-to-variable, slice-then-compare) and for complex logical expression chains (regression coverage for the NeverZero arithmetic that avoids visitor recursion).
1 parent cb88f56 commit 6cdc9ef

6 files changed

Lines changed: 267 additions & 3 deletions

File tree

packages/instrumentor/plugins/esmCodeCoverage.test.ts

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,20 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { transformSync } from "@babel/core";
17+
import { PluginItem, transformSync } from "@babel/core";
1818

19+
import { compareHooks } from "./compareHooks";
1920
import { esmCodeCoverage } from "./esmCodeCoverage";
2021
import { removeIndentation } from "./testhelpers";
2122

22-
function transform(code: string): { code: string; edgeCount: number } {
23+
function transform(
24+
code: string,
25+
extraPlugins: PluginItem[] = [],
26+
): { code: string; edgeCount: number } {
2327
const coverage = esmCodeCoverage();
2428
const result = transformSync(removeIndentation(code), {
2529
filename: "test-module.mjs",
26-
plugins: [coverage.plugin],
30+
plugins: [coverage.plugin, ...extraPlugins],
2731
});
2832
return {
2933
code: removeIndentation(result?.code),
@@ -109,4 +113,110 @@ describe("ESM code coverage instrumentation", () => {
109113
const { edgeCount } = transform(`|const x = 42;`);
110114
expect(edgeCount).toBe(0);
111115
});
116+
117+
describe("combined with compareHooks", () => {
118+
it("should replace string-literal === with traceStrCmp", () => {
119+
const { code } = transform(
120+
`
121+
|export function check(s) {
122+
| return s === "secret";
123+
|}`,
124+
[compareHooks],
125+
);
126+
127+
// The === against a string literal must be replaced.
128+
expect(code).toContain("Fuzzer.tracer.traceStrCmp");
129+
expect(code).toContain('"secret"');
130+
expect(code).toContain('"==="');
131+
// The raw === should be gone from the check expression.
132+
expect(code).not.toMatch(/s\s*===\s*"secret"/);
133+
});
134+
135+
it("should replace number-literal === with traceNumberCmp", () => {
136+
const { code } = transform(
137+
`
138+
|export function classify(n) {
139+
| if (n > 10) return "big";
140+
| if (n === 0) return "zero";
141+
| return "small";
142+
|}`,
143+
[compareHooks],
144+
);
145+
146+
expect(code).toContain("Fuzzer.tracer.traceNumberCmp");
147+
});
148+
149+
it("should NOT hook variable-to-variable comparisons", () => {
150+
// compareHooks only fires when one operand is a literal.
151+
// Comparing two identifiers is not hooked (same as CJS).
152+
const { code } = transform(
153+
`
154+
|const target = "something";
155+
|export function check(s) {
156+
| return s === target;
157+
|}`,
158+
[compareHooks],
159+
);
160+
161+
expect(code).not.toContain("Fuzzer.tracer.traceStrCmp");
162+
});
163+
164+
it("should hook slice-then-compare patterns", () => {
165+
// This is the pattern used in the integration tests.
166+
const { code } = transform(
167+
`
168+
|export function verify(s) {
169+
| if (s.slice(0, 16) === "a]3;d*F!pk29&bAc") {
170+
| throw new Error("found it");
171+
| }
172+
|}`,
173+
[compareHooks],
174+
);
175+
176+
expect(code).toContain("Fuzzer.tracer.traceStrCmp");
177+
expect(code).toContain("a]3;d*F!pk29&bAc");
178+
});
179+
180+
it("should produce both coverage and hooks together", () => {
181+
const { code, edgeCount } = transform(
182+
`
183+
|export function check(s) {
184+
| if (s === "secret") {
185+
| return true;
186+
| }
187+
| return false;
188+
|}`,
189+
[compareHooks],
190+
);
191+
192+
// Coverage counters from esmCodeCoverage
193+
expect(code).toContain("__jazzer_cov[");
194+
expect(edgeCount).toBeGreaterThan(0);
195+
// Compare hooks
196+
expect(code).toContain("Fuzzer.tracer.traceStrCmp");
197+
});
198+
});
199+
200+
describe("logical expression handling", () => {
201+
it("should instrument nested logical expressions", () => {
202+
const { code, edgeCount } = transform(`
203+
|const x = a || b && c;`);
204+
205+
// Should have instrumented the leaves of the logical tree.
206+
expect(edgeCount).toBeGreaterThanOrEqual(2);
207+
expect(code).toContain("__jazzer_cov[");
208+
});
209+
210+
it("should not infinite-loop on complex logical chains", () => {
211+
// This would cause infinite recursion if the counter
212+
// expression contained || or &&.
213+
const { code, edgeCount } = transform(`
214+
|function f() {
215+
| return a || b || c || d || e;
216+
|}`);
217+
218+
expect(edgeCount).toBeGreaterThan(0);
219+
expect(code).toContain("__jazzer_cov[");
220+
});
221+
});
112222
});

tests/esm_cjs_mixed/cjs-check.cjs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2026 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+
/**
18+
* CJS module — instrumented via hookRequire (require.extensions).
19+
*
20+
* The 10-byte random string cannot be brute-forced; the fuzzer
21+
* needs the CJS compare hooks to discover it.
22+
*/
23+
function checkCjs(s) {
24+
return s === "r4Tp!mZ@8s";
25+
}
26+
27+
module.exports = { checkCjs: checkCjs };

tests/esm_cjs_mixed/esm-check.mjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2026 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+
/**
18+
* ESM module — instrumented via the ESM loader hook (module.register).
19+
*
20+
* The 10-byte random string cannot be brute-forced; the fuzzer
21+
* needs the ESM compare hooks to discover it.
22+
*/
23+
export function checkEsm(s) {
24+
return s === "Vj9!xR2#nP";
25+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2026 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+
const { FuzzTestBuilder, cleanCrashFilesIn } = require("../helpers.js");
18+
19+
// module.register() is needed for ESM loader hooks.
20+
const [major, minor] = process.versions.node.split(".").map(Number);
21+
const supportsEsmHooks = major > 20 || (major === 20 && minor >= 6);
22+
23+
const describeOrSkip = supportsEsmHooks ? describe : describe.skip;
24+
25+
describeOrSkip("Mixed CJS + ESM instrumentation", () => {
26+
afterAll(async () => {
27+
await cleanCrashFilesIn(__dirname);
28+
});
29+
30+
it("should find a secret split across a CJS and an ESM module", () => {
31+
// The fuzz target imports checkPrefix from cjs-check.cjs
32+
// (instrumented via hookRequire) and checkSuffix from
33+
// esm-check.mjs (instrumented via the ESM loader hook).
34+
// Both halves are 16-byte random string literals that can
35+
// only be discovered through their respective compare hooks.
36+
const fuzzTest = new FuzzTestBuilder()
37+
.fuzzEntryPoint("fuzz")
38+
.fuzzFile("fuzz.mjs")
39+
.dir(__dirname)
40+
.sync(true)
41+
.disableBugDetectors([".*"])
42+
.expectedErrors("Error")
43+
.runs(5000000)
44+
.seed(111994470)
45+
.build();
46+
47+
fuzzTest.execute();
48+
expect(fuzzTest.stderr).toContain("Found the mixed CJS+ESM secret!");
49+
});
50+
});

tests/esm_cjs_mixed/fuzz.mjs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2026 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+
/**
18+
* ESM fuzz target that imports from BOTH a CJS module and an ESM
19+
* module. Each function checks a 16-byte random string literal:
20+
*
21+
* - cjs-check.cjs verifies bytes 0..15 (hookRequire path)
22+
* - esm-check.mjs verifies bytes 16..31 (ESM loader path)
23+
*
24+
* Both functions are called unconditionally so that both compare
25+
* hooks fire on every fuzzing iteration, feeding libFuzzer
26+
* dictionary entries from both instrumentation paths.
27+
*/
28+
29+
import { checkCjs } from "./cjs-check.cjs";
30+
import { checkEsm } from "./esm-check.mjs";
31+
import { FuzzedDataProvider } from "@jazzer.js/core";
32+
33+
export function fuzz(data) {
34+
const fdp = new FuzzedDataProvider(data);
35+
const cjsOk = checkCjs(fdp.consumeString(10));
36+
const esmOk = checkEsm(fdp.consumeString(10));
37+
if (cjsOk && esmOk) {
38+
throw new Error("Found the mixed CJS+ESM secret!");
39+
}
40+
}

tests/esm_cjs_mixed/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "jazzerjs-esm-cjs-mixed-test",
3+
"version": "1.0.0",
4+
"description": "Integration test: fuzzer finds a secret split across CJS and ESM modules",
5+
"scripts": {
6+
"fuzz": "jest --config '{}'",
7+
"dryRun": "echo \"Skipped: requires Node >= 20.6 for ESM loader hooks\""
8+
},
9+
"devDependencies": {
10+
"@jazzer.js/core": "file:../../packages/core"
11+
}
12+
}

0 commit comments

Comments
 (0)