Skip to content

Commit 74b07b0

Browse files
committed
feat(instrumentor): register ESM source maps for stack trace rewriting
The ESM loader now generates separate source map objects instead of inline ones and registers them with the main-thread SourceMapRegistry via a preamble call. This lets source-map-support (which hooks Error.prepareStackTrace globally) remap ESM stack traces to original source positions. The preamble line-shift problem — where prepending code after Babel made all source map lines off by one — is fixed by shifting the VLQ mappings: each prepended semicolon represents an unmapped generated line, pushing all real mappings down by the correct offset.
1 parent 4ce1c25 commit 74b07b0

2 files changed

Lines changed: 38 additions & 7 deletions

File tree

packages/instrumentor/esm-loader.mts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function instrumentModule(code: string, filename: string): string | null {
126126
transformed = transformSync(code, {
127127
filename,
128128
sourceFileName: filename,
129-
sourceMaps: "inline",
129+
sourceMaps: true,
130130
plugins,
131131
sourceType: "module",
132132
});
@@ -141,13 +141,31 @@ function instrumentModule(code: string, filename: string): string | null {
141141
return null;
142142
}
143143

144-
// Prepend a one-liner that allocates this module's counter buffer
145-
// and registers it with libFuzzer via the main-thread Fuzzer global.
146-
const preamble =
147-
`const ${COUNTER_ARRAY} = ` +
148-
`Fuzzer.coverageTracker.createModuleCounters(${edges});\n`;
144+
// Build a preamble that runs on the main thread before the module
145+
// body. It allocates the per-module coverage counter buffer and,
146+
// when a source map is available, registers it with the main-thread
147+
// SourceMapRegistry so that source-map-support can remap stack
148+
// traces back to the original source.
149+
const preambleLines = [
150+
`const ${COUNTER_ARRAY} = Fuzzer.coverageTracker.createModuleCounters(${edges});`,
151+
];
152+
153+
if (transformed.map) {
154+
// Shift the source map to account for the preamble lines we are
155+
// about to prepend. In VLQ-encoded mappings each semicolon
156+
// represents one generated line; prepending them pushes all real
157+
// mappings down by the right amount.
158+
const preambleOffset = preambleLines.length + 1; // +1 for the registration line itself
159+
const shifted = {
160+
...transformed.map,
161+
mappings: ";".repeat(preambleOffset) + transformed.map.mappings,
162+
};
163+
preambleLines.push(
164+
`__jazzer_registerSourceMap(${JSON.stringify(filename)}, ${JSON.stringify(shifted)});`,
165+
);
166+
}
149167

150-
return preamble + transformed.code;
168+
return preambleLines.join("\n") + "\n" + transformed.code;
151169
}
152170

153171
// ── Include / exclude filtering ──────────────────────────────────

packages/instrumentor/instrument.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ export class Instrumentor {
7272
if (this.includes.includes("jazzer.js")) {
7373
this.unloadInternalModules();
7474
}
75+
76+
// Expose a registration function so ESM modules can feed their
77+
// source maps back to the main-thread registry. The ESM loader
78+
// thread cannot access this registry directly, but the preamble
79+
// code it emits runs on the main thread during module evaluation
80+
// — before the module body, and therefore before any error could
81+
// need the map for stack-trace rewriting.
82+
const registry = this.sourceMapRegistry;
83+
(globalThis as Record<string, unknown>).__jazzer_registerSourceMap = (
84+
filename: string,
85+
map: SourceMap,
86+
) => registry.registerSourceMap(filename, map);
87+
7588
return this.sourceMapRegistry.installSourceMapSupport();
7689
}
7790

0 commit comments

Comments
 (0)