Main focus lies on fuzzing Node.js libraries and backend applications using libFuzzer.
Support for:
- Linux x86_64
- macOS x86_64 and arm64
- Windows x86_64
Out of scope:
- Other runtimes or browser support
From an architectural view the fuzzer consists of multiple components and also needs user provided parts. These components and their interactions will be discussed in this section.
The fuzzing library is the main entry point and is used to start fuzzing. It is invoked with the name of a fuzz target module and possible fuzzer options. As a first step it loads a native plugin Node.js addon, fuzzer plugin, to interact with libFuzzer and registers instrumentation hooks to transform subsequently loaded code.
For CJS modules, a require hook (via istanbul-lib-hook) intercepts loading.
For ES modules (Node >= 20.6), a loader hook registered via module.register()
intercepts import statements in a dedicated loader thread.
Both paths use Babel to transform source code, inserting
coverage counters so the fuzzer can detect new code paths, and comparison hooks
(for ==, !=, etc.) so the fuzzer can mutate input toward specific values.
Available feedback methods are defined in libFuzzer's
hook interface definitions.
After the initial setup fuzzer library loads the fuzz target, a module
exporting a fuzz function, which in turn has to call the actual code under
test using the provided fuzzing data. Note that the loaded code in instrumented
by instrumentor and will provide fuzzer feedback.
During the actual fuzzing run the fuzzer repeatedly invokes the fuzz function
and mutates the provided data based on feedback from the instrumented code.
Once a crash is found it logs it on the console and stops.
Context: To interact with the fuzzer there needs to be a way to initialize libFuzzer, receive fuzzing data in the fuzz target and invoke the fuzzer's callback functions. Possible solutions could be a direct integration into the V8 runtime or using a Node.js based approach.
Decision: A Node.js based approach seems to be sufficient for our purpose. Native plugins provide a stable ABI to interact with V8, JavaScript code and vice versa. We will use that as base for the fuzzer.
Consequences: No other runtime can be supported, this includes browsers.
Context: libFuzzer is a coverage-based fuzzer and hence needs coverage information to detect progress and comparison hints to improve mutations. These feedback mechanisms have to be added to the application code without user intervention.
Decision: Use istanbul-lib-hook to hook into CJS loading and
module.register() to hook into ESM loading, with Babel plugins to insert
feedback instrumentation. Instrumenting at the source level decouples from the
underlying JavaScript engine and allows fine-grained control over which modules
are instrumented.
Consequences: Independence of JavaScript engine, fine-grained instrumentation control, ESM support on Node >= 20.6.
The most crucial inner mechanisms that make Jazzer.js work are highlighted in the below Figure:

