Skip to content

Commit 5533083

Browse files
committed
fix(core): normalize LibAFL engine behavior
Store engine aliases canonically, replay every regression input, and reject malformed LibAFL integer flags so backend selection stays predictable across CLI and Jest runs.
1 parent a2104e3 commit 5533083

4 files changed

Lines changed: 59 additions & 9 deletions

File tree

packages/core/core.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,11 @@ export async function startFuzzing(
228228
registerEsmLoaderHooks(instrumentor);
229229
instrumentor.sendHooksToLoader();
230230
const fuzzFn = await loadFuzzFunction(options);
231-
const findingAwareFuzzFn = asFindingAwareFuzzFn(fuzzFn);
231+
const findingAwareFuzzFn = asFindingAwareFuzzFn(
232+
fuzzFn,
233+
true,
234+
options.get("engine"),
235+
);
232236
return startFuzzingNoInit(findingAwareFuzzFn, options).finally(() => {
233237
// These post fuzzing actions are only required for invocations through the CLI,
234238
// other means of invocation, e.g. via Jest, don't need them.
@@ -394,13 +398,11 @@ async function loadFuzzFunction(
394398
export function asFindingAwareFuzzFn(
395399
originalFuzzFn: fuzzer.FuzzTarget,
396400
dumpCrashingInput = true,
401+
engine = "libfuzzer",
397402
): FindingAwareFuzzTarget {
398403
function printAndDump(error: unknown): void {
399404
cleanErrorStack(error);
400-
const shouldDumpWithLibFuzzer =
401-
(
402-
globalThis as typeof globalThis & { options?: OptionsManager }
403-
).options?.get("engine") !== "libafl";
405+
const shouldDumpWithLibFuzzer = resolveEngine(engine) === "libfuzzer";
404406
if (
405407
!(
406408
error instanceof FuzzerSignalFinding &&

packages/core/options.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,15 @@ describe("libafl options", () => {
337337
expect(() => resolveEngine("unknown")).toThrow("Unknown fuzzing engine");
338338
});
339339

340+
it("canonicalizes engine aliases during option merge", () => {
341+
const manager = new OptionsManager(OptionSource.DefaultJestOptions).merge(
342+
{ engine: "afl" },
343+
OptionSource.ConfigurationFile,
344+
);
345+
346+
expect(manager.get("engine")).toBe("libafl");
347+
});
348+
340349
it("builds structured LibAFL options from fuzzer options", () => {
341350
const manager = new OptionsManager(OptionSource.DefaultCLIOptions).merge(
342351
{
@@ -385,7 +394,7 @@ describe("libafl options", () => {
385394
{
386395
engine: "libafl",
387396
mode: "regression",
388-
fuzzerOptions: ["corpus"],
397+
fuzzerOptions: ["corpus", "-runs=1"],
389398
},
390399
OptionSource.CommandLineArguments,
391400
);
@@ -437,6 +446,20 @@ describe("libafl options", () => {
437446
fs.rmSync(tempDirectory, { force: true, recursive: true });
438447
}
439448
});
449+
450+
it("rejects malformed LibAFL integer flags", () => {
451+
for (const option of ["-runs=1abc", "-max_len=1.5", "-seed="]) {
452+
const manager = new OptionsManager(OptionSource.DefaultCLIOptions).merge(
453+
{
454+
engine: "libafl",
455+
fuzzerOptions: [option],
456+
},
457+
OptionSource.CommandLineArguments,
458+
);
459+
460+
expect(() => buildLibAflOptions(manager)).toThrow();
461+
}
462+
});
440463
});
441464

442465
function expectDefaultsExceptKeys(

packages/core/options.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export function resolveEngine(engine: string): FuzzingEngine {
177177
return "libafl";
178178
default:
179179
throw new Error(
180-
`Unknown fuzzing engine '${engine}'. Supported engines are 'libfuzzer' and 'afl'.`,
180+
`Unknown fuzzing engine '${engine}'. Supported engines are 'libfuzzer' and 'libafl' (alias 'afl').`,
181181
);
182182
}
183183
}
@@ -336,6 +336,9 @@ export class OptionsManager {
336336
`Invalid type for Jazzer.js option '${key}', expected type '${keyType}', got '${typeof resultValue}'`,
337337
);
338338
}
339+
if (key === "engine") {
340+
resultValue = resolveEngine(resultValue);
341+
}
339342
// Deep copy the new value to avoid reference keeping and unintended mutations.
340343
resultValue = OptionsManager.copyOptionValue(resultValue);
341344
setProperty(this._options, key, { value: resultValue, source: source });
@@ -529,6 +532,12 @@ export function buildLibAflOptions(options: OptionsManager): LibAflOptions {
529532
);
530533
}
531534

535+
if (options.get("mode") === "regression") {
536+
// Regression mode should replay every available corpus input unless the
537+
// user asked to stop for some other reason, mirroring libFuzzer's behavior.
538+
runs = 0;
539+
}
540+
532541
printOptions(options);
533542
if (process.env.JAZZER_DEBUG) {
534543
console.error(
@@ -564,7 +573,7 @@ export function buildLibAflOptions(options: OptionsManager): LibAflOptions {
564573
}
565574

566575
function parsePositiveInteger(name: string, value: string): number {
567-
const parsed = Number.parseInt(value, 10);
576+
const parsed = parseDecimalInteger(name, value);
568577
if (!Number.isInteger(parsed) || parsed <= 0) {
569578
throw new Error(
570579
`Option '${name}' must be a positive integer, got '${value}'`,
@@ -574,7 +583,7 @@ function parsePositiveInteger(name: string, value: string): number {
574583
}
575584

576585
function parsePositiveOrZeroInteger(name: string, value: string): number {
577-
const parsed = Number.parseInt(value, 10);
586+
const parsed = parseDecimalInteger(name, value);
578587
if (!Number.isInteger(parsed) || parsed < 0) {
579588
throw new Error(
580589
`Option '${name}' must be a non-negative integer, got '${value}'`,
@@ -583,6 +592,21 @@ function parsePositiveOrZeroInteger(name: string, value: string): number {
583592
return parsed;
584593
}
585594

595+
function parseDecimalInteger(name: string, value: string): number {
596+
if (!/^\d+$/.test(value)) {
597+
throw new Error(`Option '${name}' must be an integer, got '${value}'`);
598+
}
599+
600+
const parsed = Number(value);
601+
if (!Number.isSafeInteger(parsed)) {
602+
throw new Error(
603+
`Option '${name}' must fit into a safe integer, got '${value}'`,
604+
);
605+
}
606+
607+
return parsed;
608+
}
609+
586610
export function printOptions(options: OptionsManager, infix = "") {
587611
if (process.env.JAZZER_DEBUG) {
588612
console.error(

packages/jest-runner/fuzz.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export function fuzz(
140140
const wrappedFn = asFindingAwareFuzzFn(
141141
fn,
142142
localConfig.get("mode") === "fuzzing",
143+
localConfig.get("engine"),
143144
);
144145

145146
if (localConfig.get("mode") === "regression") {

0 commit comments

Comments
 (0)