Skip to content
Draft

Afl #881

Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 14 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 Down Expand Up @@ -59,14 +63,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 +104,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