Skip to content

Commit c4a5b94

Browse files
committed
feat(instrumentor): seed compare-hook PRNG from libFuzzer seed
Replace crypto.randomInt(512) in fakePC() with a deterministic xorshift32 PRNG seeded from the libFuzzer -seed= value. This makes TORC slot assignments identical across runs, so a given seed produces the exact same mutation schedule every time. If no seed is provided, one is generated, injected into the libFuzzer args, and printed — giving full reproducibility from a single seed. The seed flows from the CLI through the Instrumentor to both the CJS transform path (main thread) and the ESM loader (via module.register initialization data).
1 parent 32607de commit c4a5b94

4 files changed

Lines changed: 51 additions & 3 deletions

File tree

packages/core/core.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,30 @@ declare global {
8282
var options: OptionsManager;
8383
}
8484

85+
/**
86+
* Extract -seed=N from the libFuzzer arguments. If absent, generate
87+
* a random seed and inject it so both the instrumentation layer and
88+
* libFuzzer use the same value — making the entire run reproducible
89+
* from a single seed.
90+
*/
91+
function resolveInstrumentationSeed(options: OptionsManager): number {
92+
const fuzzerOpts = options.get("fuzzerOptions");
93+
const seedArg = fuzzerOpts.find((a: string) => a.startsWith("-seed="));
94+
if (seedArg) {
95+
const parsed = parseInt(seedArg.split("=")[1], 10);
96+
// libFuzzer treats -seed=0 as "pick random", so we do the same.
97+
if (parsed !== 0) return parsed;
98+
}
99+
const generated = Math.floor(Math.random() * 0x7fff_fffe) + 1; // [1, 2^31-1]
100+
fuzzerOpts.push(`-seed=${generated}`);
101+
console.error(`INFO: Using generated seed: ${generated}`);
102+
return generated;
103+
}
104+
85105
export async function initFuzzing(
86106
options: OptionsManager,
87107
): Promise<Instrumentor> {
108+
const seed = resolveInstrumentationSeed(options);
88109
const instrumentor = new Instrumentor(
89110
options.get("includes"),
90111
options.get("excludes"),
@@ -94,6 +115,8 @@ export async function initFuzzing(
94115
options.get("idSyncFile")
95116
? new FileSyncIdStrategy(options.get("idSyncFile"))
96117
: new MemorySyncIdStrategy(),
118+
undefined, // sourceMapRegistry — use default
119+
seed,
97120
);
98121
registerInstrumentor(instrumentor);
99122

packages/instrumentor/esm-loader.mts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ const { functionHooks } =
4848
// it with stub hooks from the serialized data we receive via the port.
4949
const { hookManager: loaderHookManager } =
5050
require("@jazzer.js/hooking") as typeof import("@jazzer.js/hooking");
51+
const { setSeed } =
52+
require("./plugins/helpers.js") as typeof import("./plugins/helpers.js");
5153

5254
// Already-instrumented code contains this marker.
5355
const INSTRUMENTATION_MARKER = "Fuzzer.coverageTracker.incrementCounter";
@@ -59,6 +61,7 @@ interface LoaderConfig {
5961
includes: string[];
6062
excludes: string[];
6163
coverage: boolean;
64+
seed?: number;
6265
port?: MessagePort;
6366
}
6467

@@ -67,6 +70,9 @@ let loaderPort: MessagePort | null = null;
6770

6871
export function initialize(data: LoaderConfig): void {
6972
config = data;
73+
if (data.seed != null) {
74+
setSeed(data.seed);
75+
}
7076
if (data.port) {
7177
loaderPort = data.port;
7278
}

packages/instrumentor/instrument.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { instrumentationPlugins } from "./plugin";
3333
import { codeCoverage } from "./plugins/codeCoverage";
3434
import { compareHooks } from "./plugins/compareHooks";
3535
import { functionHooks } from "./plugins/functionHooks";
36+
import { setSeed } from "./plugins/helpers";
3637
import { sourceCodeCoverage } from "./plugins/sourceCodeCoverage";
3738
import {
3839
extractInlineSourceMap,
@@ -74,6 +75,7 @@ export class Instrumentor {
7475
private readonly isDryRun = false,
7576
private readonly idStrategy: EdgeIdStrategy = new MemorySyncIdStrategy(),
7677
private readonly sourceMapRegistry: SourceMapRegistry = new SourceMapRegistry(),
78+
private readonly _seed: number = 0xdead_beef,
7779
) {
7880
// This is our default case where we want to include everything and exclude the "node_modules" folder.
7981
if (includes.length === 0 && excludes.length === 0) {
@@ -85,6 +87,8 @@ export class Instrumentor {
8587
}
8688

8789
init(): () => void {
90+
setSeed(this._seed);
91+
8892
if (this.includes.includes("jazzer.js")) {
8993
this.unloadInternalModules();
9094
}
@@ -231,6 +235,10 @@ export class Instrumentor {
231235
return this.shouldCollectSourceCodeCoverage;
232236
}
233237

238+
get seed(): number {
239+
return this._seed;
240+
}
241+
234242
/** Connect the main-thread side of the loader MessagePort. */
235243
setLoaderPort(port: MessagePort): void {
236244
this.loaderPort = port;
@@ -346,6 +354,7 @@ function registerEsmHooks(instrumentor: Instrumentor): void {
346354
includes: instrumentor.includePatterns,
347355
excludes: instrumentor.excludePatterns,
348356
coverage: instrumentor.coverageEnabled,
357+
seed: instrumentor.seed,
349358
};
350359

351360
const options: {

packages/instrumentor/plugins/helpers.ts

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

17-
import * as crypto from "crypto";
18-
1917
import { types } from "@babel/core";
2018
import { NumericLiteral } from "@babel/types";
2119

20+
// xorshift32 PRNG for deterministic compare-hook TORC slot assignments.
21+
// Seeded once at startup so that a given -seed= value produces identical
22+
// mutation schedules across runs.
23+
let state = 0xdead_beef;
24+
25+
export function setSeed(seed: number): void {
26+
state = seed | 1; // xorshift requires non-zero state
27+
}
28+
2229
export function fakePC(): NumericLiteral {
23-
return types.numericLiteral(crypto.randomInt(512));
30+
state ^= state << 13;
31+
state ^= state >> 17;
32+
state ^= state << 5;
33+
return types.numericLiteral((state >>> 0) % 512);
2434
}

0 commit comments

Comments
 (0)