Canister Fuzzing Framework
A coverage-guided fuzzer for Internet Computer canisters, built on libafl and pocket-ic. It finds bugs by instrumenting canister Wasm, emulating the IC with pocket-ic, and using libafl to explore code paths.
Building a new fuzzer
To build a fuzzer, one must implement the FuzzerOrchestrator trait. This involves two main parts: an init function to set up the canisters and an execute function that runs for each input.
// my_fuzzer/src/main.rs
use ;
use FuzzerOrchestrator;
use parse_canister_result_for_trap;
use ExitKind;
use BytesInput;
use Principal;
// 1. Define a struct for the fuzzer state using the macro
define_fuzzer_state!;
// 2. Implement the core fuzzing logic
// 4. The main function to configure and run the fuzzer
Running an example
Prerequisites
- Rust:
- wasi2ic (for WASI-based canisters like
rusqlite_db): - DFX: Installation guide (for Motoko canisters).
- Mops:
npm install -g mops(for Motoko canisters).
The examples/ directory contains sample fuzzers. To run the stable_memory_ops example:
-
Build and Run:
-
Check Output: The fuzzer will start and display a status screen. Results, including new inputs (
corpus) and crashes, are saved to a timestamped directory insideartifacts/. The exact path is printed at startup.
Reproduce a Crash
When a crash is found, the input is saved to the artifacts/.../crashes/ directory. Use the test_one_input method to reproduce it for debugging.
-
Find the Crash File: Copy the path to a crash input file from the fuzzer's output directory.
-
Modify
mainto Test One Input:use fs;
How It Works
The framework connects three components:
-
pocket-ic(IC Emulator) — Runs canisters locally in-process. The fuzzer installs instrumented Wasm intopocket-icand makes canister calls for each test input. -
Wasm Instrumentation — Before deployment, the target canister's Wasm module is transformed to provide execution feedback:
-
Branch coverage: An AFL-style instrumentation pass injects code at every basic block and branch. Each instrumentation point updates a shared coverage map using XOR-based edge hashing with a configurable history size.
-
Coverage export: A special method (
__export_coverage_for_afl) is added to the Wasm module so the fuzzer can retrieve the coverage map after each execution. -
Instruction count maximization (optional): When
instrument_instruction_count: trueis set, wrapper functions are injected around eachcanister_updateexport. The wrappers readic0.performance_counterafter the original method returns and subtract the estimated AFL instrumentation overhead. A separate export (__export_instruction_count_for_afl) lets the fuzzer retrieve the count. Combined withinstruction_config()returningInstructionConfig { enabled: true, .. }inFuzzerOrchestrator, this guides the fuzzer toward inputs that consume the most IC instructions — no changes to the target canister's source code required. Each new maximum is logged with a timestamp, instruction count, and input hex preview, and the input is saved to the corpus directory for replay. Settingmax_instruction_countto a threshold will treat inputs that exceed it as crashes. See thedecode_candid_by_instructionsexample.
-
-
libafl(Fuzzing Engine) — Drives the main loop: generating inputs, executing them viapocket-ic, collecting coverage (and optionally instruction count) feedback, and managing the corpus. The framework also includes a Candid-aware mutator that can parse.didfiles and perform structure-aware mutations on Candid-encoded inputs.
License
This project is licensed under the Apache-2.0 License.