Skip to content
Draft

Afl #881

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ae1d1e1
refactor(options): extract shared option contracts
oetr Apr 30, 2026
bd19af3
feat(core): add LibAFL as a fuzzing backend
oetr Apr 30, 2026
462a273
test(engine): cover backend selection end to end
oetr Apr 30, 2026
da53022
feat(fuzzer): add compare-guided mutation for LibAFL
oetr Apr 30, 2026
be90f36
fix(fuzzer): align LibAFL containment tracing
oetr Apr 30, 2026
935ed95
feat(fuzzer): tune LibAFL queue scheduling
oetr Apr 30, 2026
873f764
feat(fuzzer): add structured LibAFL progress output
oetr Apr 30, 2026
5c16b7b
fix(instrumentor): preserve lazy ESM coverage in LibAFL
oetr Apr 30, 2026
85b8e96
refactor(fuzzer): split LibAFL runtime helpers
oetr Apr 30, 2026
ee05aa0
fix(fuzzer): serialize LibAFL async teardown
oetr Apr 30, 2026
0bfe46b
test(bench): add LibAFL smoke and anomaly checks
oetr Apr 30, 2026
4222fa7
test: cap root Jest worker usage
oetr Apr 30, 2026
d57646c
ci(fuzzer): build the Rust runtime in CI
oetr Apr 30, 2026
00ff007
docs: document backend-specific engine usage
oetr Apr 30, 2026
56b3aa4
fix(fuzzer): reject invalid coverage counter ranges
oetr Apr 30, 2026
c2e2bbf
fix(core): reject invalid fuzzing modes early
oetr Apr 30, 2026
a4eb823
fix(fuzzer): contain sync stop callback errors
oetr Apr 30, 2026
2853b46
fix(core): restore libFuzzer as the CLI default
oetr Apr 30, 2026
4ae125c
refactor(options): align extracted options with LibAFL
oetr Apr 30, 2026
7919e2f
refactor: fuzzer mode is unified
oetr Apr 30, 2026
311fa87
afl: use unified fuzzer mode FIXUP
oetr Apr 30, 2026
2b03ec9
test(fuzzer): skip windows SIGINT stop test
oetr May 4, 2026
36da2b5
ci: cap unit test jobs at 30 minutes
oetr May 4, 2026
88910e0
chore: support building with clang with older glibc
oetr May 4, 2026
00ec13d
test(fuzzer): keep direct coverage buffers alive
oetr May 4, 2026
2387a64
fix(fuzzer): restore legacy libFuzzer handlers
oetr May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ jobs:
with:
node-version: 22
cache: "npm"
- name: rust
uses: dtolnay/rust-toolchain@stable
- name: rust target (macos x64)
if: ${{ matrix.os == 'macos-latest' && matrix.arch == '--arch x86_64' }}
run: rustup target add x86_64-apple-darwin
- name: MSVC (windows)
uses: ilammy/msvc-dev-cmd@v1
if: contains(matrix.os, 'windows')
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/run-all-tests-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,25 @@ jobs:
path: |
packages/fuzzer/prebuilds
key:
fuzzer-cache-${{ runner.os }}-${{
hashFiles('packages/fuzzer/CMakeLists.txt',
'packages/fuzzer/**/*.h', 'packages/fuzzer/**/*.cpp') }}
fuzzer-cache-${{ runner.os }}-${{ hashFiles('package-lock.json',
'package.json', 'packages/fuzzer/package.json',
'packages/fuzzer/CMakeLists.txt', 'packages/fuzzer/**/*.h',
'packages/fuzzer/**/*.cpp', 'packages/fuzzer/rust/Cargo.toml',
'packages/fuzzer/rust/Cargo.lock',
'packages/fuzzer/rust/src/**/*.rs') }}
- name: node
uses: actions/setup-node@v6
with:
node-version: 22
cache: "npm"
- name: rust
uses: dtolnay/rust-toolchain@stable
- name: install dependencies
run: npm ci
- name: build project
run: npm run build
- name: test LibAFL runtime
run: cargo test --manifest-path packages/fuzzer/rust/Cargo.toml
- name: build fuzzer
if: ${{ steps.cache-fuzzer.outputs.cache-hit != 'true' }}
run: npm run build --workspace=@jazzer.js/fuzzer
Expand Down
18 changes: 15 additions & 3 deletions .github/workflows/run-all-tests-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ jobs:
with:
node-version: 22
cache: "npm"
- name: rust
uses: dtolnay/rust-toolchain@stable
- name: install dependencies
run: npm ci
- name: install clang-tidy
run: sudo apt-get install -y clang-tidy
- name: build project
# Build project so that imports can be checked during linting
run: npm run build
- name: test LibAFL runtime
run: cargo test --manifest-path packages/fuzzer/rust/Cargo.toml
- name: build fuzzer
# Build the native addon so that CMake downloads libFuzzer and
# generates compile_commands.json, which are needed by clang-tidy
Expand All @@ -40,6 +44,7 @@ jobs:
tests:
name: unit tests
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
matrix:
os: [windows-latest, ubuntu-latest, ubuntu-24.04-arm, macos-latest]
Expand All @@ -59,14 +64,19 @@ jobs:
path: |
packages/fuzzer/prebuilds
key:
fuzzer-cache-${{ matrix.os }}-${{
hashFiles('packages/fuzzer/CMakeLists.txt',
'packages/fuzzer/**/*.h', 'packages/fuzzer/**/*.cpp') }}
fuzzer-cache-${{ matrix.os }}-${{ hashFiles('package-lock.json',
'package.json', 'packages/fuzzer/package.json',
'packages/fuzzer/CMakeLists.txt', 'packages/fuzzer/**/*.h',
'packages/fuzzer/**/*.cpp', 'packages/fuzzer/rust/Cargo.toml',
'packages/fuzzer/rust/Cargo.lock',
'packages/fuzzer/rust/src/**/*.rs') }}
- name: node
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: "npm"
- name: rust
uses: dtolnay/rust-toolchain@stable
- name: MSVC (windows)
uses: ilammy/msvc-dev-cmd@v1
if: contains(matrix.os, 'windows')
Expand Down Expand Up @@ -95,6 +105,8 @@ jobs:
with:
node-version: 22
cache: "npm"
- name: rust
uses: dtolnay/rust-toolchain@stable
- name: MSVC (windows)
uses: ilammy/msvc-dev-cmd@v1
if: contains(matrix.os, 'windows')
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ node_modules
.idea
.vscode
compile_commands.json
packages/fuzzer/rust/target
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@

Jazzer.js is a coverage-guided, in-process fuzzer for the
[Node.js](https://nodejs.org) platform developed by
[Code Intelligence](https://www.code-intelligence.com). It is based on
[libFuzzer](https://llvm.org/docs/LibFuzzer.html) and brings many of its
[Code Intelligence](https://www.code-intelligence.com). It supports
[libFuzzer](https://llvm.org/docs/LibFuzzer.html) and
[LibAFL](https://github.com/AFLplusplus/LibAFL) backends and brings
instrumentation-powered mutation features to the JavaScript ecosystem.

## Quickstart
Expand Down Expand Up @@ -47,6 +48,9 @@ To use Jazzer.js in your own project follow these few simple steps:
npx jazzer FuzzTarget
```

CLI fuzzing uses the LibAFL backend by default. To run with libFuzzer
instead, add `--engine=libfuzzer`.

4. Enjoy fuzzing!

## Usage
Expand Down
3 changes: 3 additions & 0 deletions benchmarks/engine_smoke/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
package-lock.json
work/
189 changes: 189 additions & 0 deletions benchmarks/engine_smoke/anomaly.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright 2026 Code Intelligence GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const { spawnSync } = require("child_process");
const fs = require("fs");
const path = require("path");

const benchmarkDirectory = __dirname;
const workDirectory = path.join(benchmarkDirectory, "work", "anomalies");
const engineTarget = path.join(
benchmarkDirectory,
"..",
"..",
"tests",
"engine",
"fuzz.js",
);
const asyncTarget = path.join(benchmarkDirectory, "anomaly_fuzz.js");

function removeIfExists(targetPath) {
fs.rmSync(targetPath, { force: true, recursive: true });
}

function ensureDirectory(targetPath) {
fs.mkdirSync(targetPath, { recursive: true });
}

function runCommand(label, args, cwd, outputDirectory, expectedStatus = 0) {
console.log(`\n[anomaly] ${label}`);
console.log(`[anomaly] command: npx ${args.join(" ")}`);
ensureDirectory(outputDirectory);
const sanitizedLabel = label.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
const stdoutPath = path.join(outputDirectory, `${sanitizedLabel}.stdout.log`);
const stderrPath = path.join(outputDirectory, `${sanitizedLabel}.stderr.log`);
const stdoutFd = fs.openSync(stdoutPath, "w");
const stderrFd = fs.openSync(stderrPath, "w");
const startedAt = Date.now();
const proc = spawnSync("npx", args, {
cwd,
env: { ...process.env },
shell: true,
stdio: ["ignore", stdoutFd, stderrFd],
windowsHide: true,
});
const elapsedMs = Date.now() - startedAt;
fs.closeSync(stdoutFd);
fs.closeSync(stderrFd);

if (proc.status !== expectedStatus) {
throw new Error(
`${label} failed with exit code ${proc.status}\nSTDOUT (${stdoutPath}):\n${fs.readFileSync(stdoutPath, "utf8")}\nSTDERR (${stderrPath}):\n${fs.readFileSync(stderrPath, "utf8")}`,
);
}

return {
elapsedMs,
stderrPath,
stdoutPath,
};
}

function parseExecsPerSecond(stderrPath) {
const stderr = fs.readFileSync(stderrPath, "utf8");
const match = stderr.match(/speed:\s+([0-9.]+) exec\/s/);
if (!match) {
throw new Error(`No LibAFL done line found in ${stderrPath}`);
}
return Number.parseFloat(match[1]);
}

function runGuidedNumericSmoke() {
const outputDirectory = path.join(workDirectory, "guided-numeric");
const corpusDirectory = path.join(outputDirectory, "corpus");
removeIfExists(outputDirectory);
ensureDirectory(corpusDirectory);
fs.writeFileSync(path.join(corpusDirectory, "seed"), Buffer.alloc(4));

const result = runCommand(
"guided numeric solve",
[
"jazzer",
engineTarget,
"-f",
"guided_numeric",
"--engine=afl",
"--sync",
"--disable_bug_detectors=.*",
"--",
corpusDirectory,
"-runs=4000",
"-seed=1337",
"-max_len=16",
`-artifact_prefix=${outputDirectory}${path.sep}`,
],
benchmarkDirectory,
outputDirectory,
77,
);

const output =
fs.readFileSync(result.stdoutPath, "utf8") +
fs.readFileSync(result.stderrPath, "utf8");
if (!output.includes("AFL numeric guidance finding")) {
throw new Error("Guided numeric smoke did not report the expected finding");
}

return {
name: "guided-numeric",
elapsedMs: result.elapsedMs,
};
}

function runAsyncSmoke() {
const outputDirectory = path.join(workDirectory, "async-smoke");
const corpusDirectory = path.join(outputDirectory, "corpus");
removeIfExists(outputDirectory);
ensureDirectory(corpusDirectory);
fs.writeFileSync(path.join(corpusDirectory, "seed"), "async-seed");

const result = runCommand(
"async throughput smoke",
[
"jazzer",
asyncTarget,
"-f",
"async_smoke",
"--engine=afl",
"--disable_bug_detectors=.*",
"--",
corpusDirectory,
"-runs=2000",
"-seed=9001",
"-max_len=128",
`-artifact_prefix=${outputDirectory}${path.sep}`,
],
benchmarkDirectory,
outputDirectory,
);

const execsPerSecond = parseExecsPerSecond(result.stderrPath);
if (execsPerSecond <= 0) {
throw new Error("Async smoke reported a non-positive exec/sec rate");
}
if (result.elapsedMs > 30000) {
throw new Error(
`Async smoke took unexpectedly long: ${result.elapsedMs} ms`,
);
}

return {
name: "async-smoke",
elapsedMs: result.elapsedMs,
execsPerSecond,
};
}

function main() {
ensureDirectory(workDirectory);
const results = [runGuidedNumericSmoke(), runAsyncSmoke()];
for (const result of results) {
const stats = [`elapsed_ms=${result.elapsedMs}`];
if (result.execsPerSecond !== undefined) {
stats.push(`execs_per_second=${result.execsPerSecond}`);
}
console.log(`[anomaly] ${result.name}: ${stats.join(" ")}`);
}
fs.writeFileSync(
path.join(workDirectory, "results.json"),
JSON.stringify(results, null, 2),
);
console.log(
`\n[anomaly] wrote machine-readable results to ${path.join(workDirectory, "results.json")}`,
);
}

main();
32 changes: 32 additions & 0 deletions benchmarks/engine_smoke/anomaly_fuzz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2026 Code Intelligence GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

module.exports.async_smoke = function (data) {
let checksum = 0;
for (const byte of data) {
checksum = ((checksum * 33) ^ byte) & 0xffff;
}

return new Promise((resolve) => {
setImmediate(() => {
if (checksum === 0x1337) {
// Exercise an extra branch without turning this into a finding target.
checksum ^= data.length;
}
resolve(checksum);
});
});
};
Loading
Loading