Coverage-guided fuzzing for JavaScript and TypeScript, built as a Vitest plugin.
Vitiate uses SWC for compile-time instrumentation and LibAFL for mutation-driven fuzzing. Write fuzz tests alongside your unit tests in Vitest, or use the standalone CLI for libFuzzer-compatible workflows.
Install the package:
npm install --save-dev vitiateThis installs the Vitest plugin, the standalone CLI, and all required dependencies.
If you only need the Vitest plugin and not the CLI, you can install @vitiate/core instead.
Configure Vitest (vitest.config.ts):
import { defineConfig } from "vitest/config";
import { vitiatePlugin } from "@vitiate/core/plugin";
export default defineConfig({
plugins: [vitiatePlugin()],
test: {
projects: [
{ extends: true, test: { name: "unit", include: ["test/**/*.test.ts"] } },
{ extends: true, test: { name: "fuzz", include: ["test/**/*.fuzz.ts"] } },
],
},
});Write a fuzz test (test/parser.fuzz.ts):
import { fuzz } from "@vitiate/core";
import { parse, ParseError } from "../src/parser.js";
fuzz("parse does not crash", (data: Buffer) => {
try {
parse(data.toString("utf-8"));
} catch (error) {
if (!(error instanceof ParseError)) {
throw error; // re-throw unexpected errors
}
}
});Run the fuzzer:
npx vitiate fuzzOr via Vitest directly:
VITIATE_FUZZ=1 npx vitest runCrashes are saved to .vitiate/testdata/<hashdir>/crashes/crash-<sha256> (where <hashdir> is a base32 encoded name like vxr4kpqyb12fza1gv81bjj8k3i64mlqn-parse_does_not_crash).
Run your test suite normally and they are replayed as regression tests automatically.
Add the following to your .gitignore:
# Vitiate cached corpus (regenerated by the fuzzer)
.vitiate/corpus/
# SWC WASM plugin compilation cache (created by Vitiate's instrumentation)
.swc/To fuzz code inside third-party npm packages, use the instrument.packages option. The plugin automatically handles module inlining and transform configuration for listed packages.
- Transform time: Vite's plugin hooks run every JS/TS module through the SWC plugin as it is imported, inserting edge coverage counters and comparison tracing calls. No separate build step required.
- Runtime: A shared coverage map (zero-copy between JS and Rust) tracks which edges are hit.
- Fuzz loop: LibAFL reads the coverage map after each execution, evaluates feedback, updates the corpus, and generates the next input using havoc mutations, CmpLog-guided byte replacement, Grimoire structure-aware mutations, and Unicode-aware mutations.
- Crashes: Inputs that cause uncaught exceptions are saved as crash artifacts under
.vitiate/testdata/and minimized automatically.
Prebuilt native binaries are provided for:
- Linux: x86_64 (glibc, musl), aarch64 (glibc, musl), armv7 (gnueabihf)
- macOS: aarch64 (Apple Silicon)
- Windows: x86_64
Requires Node.js 18 or later, Vite 6+, and Vitest 3.1+.
| Package | Description |
|---|---|
vitiate |
Wrapper package with the vitiate CLI binary |
@vitiate/core |
Vite plugin, fuzz API, corpus management |
@vitiate/engine |
Native Node.js addon wrapping LibAFL (prebuilt binaries) |
@vitiate/swc-plugin |
SWC WASM plugin for coverage instrumentation |
@vitiate/fuzzed-data-provider |
Structured fuzzing helper (optional) |