Skip to content

Commit ad99c42

Browse files
committed
feat(fuzzer): symbolize CJS PCs with project-relative paths
Capture CJS edge source locations during instrumentation and register those mappings so libFuzzer can resolve fake PCs to real JS lines. Normalize both CJS and ESM filenames to project-relative paths before registration so NEW_PC output is concise and stable.
1 parent 7bbaab0 commit ad99c42

3 files changed

Lines changed: 94 additions & 11 deletions

File tree

packages/instrumentor/edgeIdStrategy.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ if (process.listeners) {
4040

4141
export interface EdgeIdStrategy {
4242
nextEdgeId(): number;
43+
/** Return the next edge ID that will be allocated, without consuming it. */
44+
peekNextEdgeId(): number;
4345
startForSourceFile(filename: string): void;
4446
commitIdCount(filename: string): void;
4547
}
@@ -52,6 +54,10 @@ export abstract class IncrementingEdgeIdStrategy implements EdgeIdStrategy {
5254
return this._nextEdgeId++;
5355
}
5456

57+
peekNextEdgeId(): number {
58+
return this._nextEdgeId;
59+
}
60+
5561
abstract startForSourceFile(filename: string): void;
5662
abstract commitIdCount(filename: string): void;
5763
}
@@ -241,6 +247,10 @@ export class ZeroEdgeIdStrategy implements EdgeIdStrategy {
241247
return 0;
242248
}
243249

250+
peekNextEdgeId(): number {
251+
return 0;
252+
}
253+
244254
startForSourceFile(filename: string): void {
245255
// Nothing to do here
246256
}

packages/instrumentor/instrument.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ import {
2626
} from "@babel/core";
2727
import { hookRequire, TransformerOptions } from "istanbul-lib-hook";
2828

29+
import { fuzzer } from "@jazzer.js/fuzzer";
2930
import { hookManager, HookType } from "@jazzer.js/hooking";
3031

3132
import { EdgeIdStrategy, MemorySyncIdStrategy } from "./edgeIdStrategy";
3233
import { instrumentationPlugins } from "./plugin";
33-
import { codeCoverage } from "./plugins/codeCoverage";
34+
import { cjsCoverage, CjsCoverageResult } from "./plugins/codeCoverage";
3435
import { compareHooks } from "./plugins/compareHooks";
3536
import { functionHooks } from "./plugins/functionHooks";
3637
import { sourceCodeCoverage } from "./plugins/sourceCodeCoverage";
@@ -63,8 +64,20 @@ export interface SerializedHook {
6364
async: boolean;
6465
}
6566

67+
const PROJECT_ROOT_PREFIX = (() => {
68+
const cwd = path.resolve(process.cwd());
69+
return cwd.endsWith(path.sep) ? cwd : `${cwd}${path.sep}`;
70+
})();
71+
72+
function stripProjectRootPrefix(filename: string): string {
73+
return filename.startsWith(PROJECT_ROOT_PREFIX)
74+
? filename.slice(PROJECT_ROOT_PREFIX.length)
75+
: filename;
76+
}
77+
6678
export class Instrumentor {
6779
private loaderPort: MessagePort | null = null;
80+
private readonly cjsCoverage: CjsCoverageResult;
6881

6982
constructor(
7083
private readonly includes: string[] = [],
@@ -82,6 +95,7 @@ export class Instrumentor {
8295
}
8396
this.includes = Instrumentor.cleanup(includes);
8497
this.excludes = Instrumentor.cleanup(excludes);
98+
this.cjsCoverage = cjsCoverage(this.idStrategy);
8599
}
86100

87101
init(): () => void {
@@ -112,9 +126,10 @@ export class Instrumentor {
112126

113127
const shouldInstrumentFile = this.shouldInstrumentForFuzzing(filename);
114128
if (shouldInstrumentFile) {
129+
this.cjsCoverage.clear();
115130
transformations.push(
116131
...instrumentationPlugins.plugins,
117-
codeCoverage(this.idStrategy),
132+
this.cjsCoverage.plugin,
118133
compareHooks,
119134
);
120135
}
@@ -154,11 +169,32 @@ export class Instrumentor {
154169
}
155170
}
156171
if (shouldInstrumentFile) {
172+
this.registerCjsPCLocations(filename);
157173
this.idStrategy.commitIdCount(filename);
158174
}
159175
return result;
160176
}
161177

178+
private registerCjsPCLocations(filename: string): void {
179+
const entries = this.cjsCoverage.edgeEntries();
180+
if (entries.length === 0) return;
181+
182+
const flat = new Int32Array(entries.length * 4);
183+
for (let i = 0; i < entries.length; i++) {
184+
const e = entries[i];
185+
flat[i * 4] = e[0];
186+
flat[i * 4 + 1] = e[1];
187+
flat[i * 4 + 2] = e[2];
188+
flat[i * 4 + 3] = e[3];
189+
}
190+
fuzzer.coverageTracker.registerPCLocations(
191+
stripProjectRootPrefix(filename),
192+
this.cjsCoverage.funcNames(),
193+
flat,
194+
0,
195+
);
196+
}
197+
162198
// eslint-disable-next-line @typescript-eslint/no-explicit-any
163199
private asInputSourceOption(inputSourceMap: any): any {
164200
// Empty input source maps mess up the coverage report.

packages/instrumentor/plugins/codeCoverage.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,52 @@ import { PluginTarget, types } from "@babel/core";
1818

1919
import { EdgeIdStrategy } from "../edgeIdStrategy";
2020

21-
import { makeCoverageVisitor } from "./coverageVisitor";
21+
import {
22+
EdgeLocation,
23+
makeCoverageVisitor,
24+
StringInterner,
25+
} from "./coverageVisitor";
26+
import type { EdgeEntry } from "./esmCodeCoverage";
2227

23-
export function codeCoverage(idStrategy: EdgeIdStrategy): () => PluginTarget {
24-
return () => ({
25-
visitor: makeCoverageVisitor(() =>
26-
types.callExpression(
27-
types.identifier("Fuzzer.coverageTracker.incrementCounter"),
28-
[types.numericLiteral(idStrategy.nextEdgeId())],
28+
export interface CjsCoverageResult {
29+
plugin: () => PluginTarget;
30+
/** Deduplicated function name table accumulated so far. */
31+
funcNames: () => string[];
32+
/** Edge entries accumulated since the last clear(). */
33+
edgeEntries: () => EdgeEntry[];
34+
/** Reset accumulated entries — call after registering each file's locations. */
35+
clear: () => void;
36+
}
37+
38+
export function cjsCoverage(idStrategy: EdgeIdStrategy): CjsCoverageResult {
39+
const funcNames = new StringInterner();
40+
const entries: EdgeEntry[] = [];
41+
42+
const onEdge = (loc: EdgeLocation): void => {
43+
const id = idStrategy.peekNextEdgeId();
44+
entries.push([id, loc.line, loc.col, funcNames.intern(loc.func)]);
45+
};
46+
47+
return {
48+
plugin: () => ({
49+
visitor: makeCoverageVisitor(
50+
() =>
51+
types.callExpression(
52+
types.identifier("Fuzzer.coverageTracker.incrementCounter"),
53+
[types.numericLiteral(idStrategy.nextEdgeId())],
54+
),
55+
onEdge,
2956
),
30-
),
31-
});
57+
}),
58+
funcNames: () => funcNames.strings(),
59+
edgeEntries: () => entries,
60+
clear: () => {
61+
entries.length = 0;
62+
funcNames.clear();
63+
},
64+
};
65+
}
66+
67+
export function codeCoverage(idStrategy: EdgeIdStrategy): () => PluginTarget {
68+
return cjsCoverage(idStrategy).plugin;
3269
}

0 commit comments

Comments
 (0)