We are excited to introduce canister_fuzzing(crate: canfuzz), a new open-source automated testing framework designed to help finding vulnerabilities in your canisters.
Motivation: Why Fuzzing?
Fuzzing is a dynamic software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer program. Modern fuzzing frameworks like AFL have a great track record of finding bugs in complex codebases and it has also proven to work in smart contract environments like echidna with EVM toolchain.
Traditional fuzzers work well for standard binaries, but fuzzing IC canisters presents unique challenges:
- The Environment: Canisters run in a WebAssembly sandbox with specific system APIs (like stable memory or inter-canister calls).
- State: Canisters are stateful actors. A bug might only trigger after a specific sequence of updates, not just a single call.
We built the Canister Fuzzing Framework to bridge this gap. It allows you to apply state-of-the-art coverage-guided fuzzing techniques directly to your compiled Wasm binaries and discover vulnerabilities in your canister code.
How it works
The canister_fuzzing repository is a Rust-based library that combines two powerful tools:
- LibAFL: The industry standard for building scalable, coverage-guided fuzzers.
- PocketIC: A lightweight emulator for the Internet Computer.
Instead of blindly throwing random data (similar to a proptest) at your canister, the framework instruments your Wasm binary. It injects small tracking codes into your compiled wasm that provides stateful feedback to your fuzzing engine. In short,
- The fuzzer generates an input (e.g., a byte array).
- It spins up a
PocketICinstance and sends the input to your canister. - The instrumented Wasm reports back which lines of code were executed.
- If a new code path is found,
LibAFLsaves that input and mutates it further to explore deeper. - If the canister traps (panics) or violates a custom invariant, the fuzzer reports a Crash.
For a more technical walkthrough, please check out this Global R&D presentation.
How to Use
Getting started is straightforward. You don’t need to change your canister’s source code; you just need your compiled .wasm file and a small Rust implementation to build the fuzzer.
If you have already written integration tests with PocketIC, most of the logic is portable to the canister_fuzzing framework.
Step 1: Add the dependency
In your Cargo.toml, add the canfuzz crate:
[dependencies]
canfuzz = "0.3.0"
candid = "0.10"
Step 2: Create your Fuzzer
Create a new generic Rust binary (e.g., fuzz/src/main.rs). You need to implement the FuzzerOrchestrator trait, which tells the fuzzer how to initialize the environment and what method to call.
Here is a simplified example:
use canfuzz::fuzzer::{CanisterInfo, CanisterType, FuzzerState, WasmPath};
use canfuzz::orchestrator::{FuzzerOrchestrator, FuzzerStateProvider};
use canfuzz::libafl::executors::ExitKind;
use canfuzz::libafl::inputs::BytesInput;
use candid::Principal;
struct MyFuzzer(FuzzerState);
// Boilerplate to access state
impl FuzzerStateProvider for MyFuzzer {
fn get_fuzzer_state(&self) -> &FuzzerState { &self.0 }
}
impl FuzzerOrchestrator for MyFuzzer {
// 1. Setup the environment
fn init(&mut self) {
// Sets up PocketIC and installs canisters
}
// 2. Define the test logic
fn execute(&self, input: BytesInput) -> ExitKind {
// Convert fuzzer input to bytes
let payload: Vec<u8> = input.into();
// Get the simulated environment
let pic = self.get_state_machine();
let target_id = self.get_coverage_canister_id();
// Call your canister method
let result = pic.update_call(
target_id,
Principal::anonymous(),
"my_method_name",
payload,
);
// Check if the canister crashed (trapped)
if result.is_err() {
return ExitKind::Crash;
}
ExitKind::Ok
}
}
fn main() {
// Point to your compiled Wasm
let canisters = vec![CanisterInfo {
name: "target".to_string(),
ty: CanisterType::Coverage,
wasm_path: WasmPath::Path("path/to/my_canister.wasm".to_string()),
id: None,
}];
let mut fuzzer = MyFuzzer(FuzzerState::new("my_fuzzer", canisters));
fuzzer.run();
}
Step 3: Run the Fuzzer
Run your fuzzer using Cargo. It will compile, start up, and begin printing statistics to your terminal.
cargo run --release
You will see a dashboard showing “corpses” (interesting inputs found) and “crashes” (bugs found).
Step 4: Debugging a Crash
If the fuzzer finds a crash, it will save the specific input that caused it in the crash directory. You can reproduce the crash deterministically by adding a few lines to your main function:
use std::fs;
// ... inside main
// remove fuzzer.run();
let crash_data = fs::read("artifacts/.../crashes/id:000000...").unwrap();
fuzzer.test_one_input(crash_data); // Replays the exact crash
4. Additional Links
- GitHub Repository: dfinity/canister_fuzzing. The repository contains examples to get you started.
- Crate on Crates.io: canfuzz
Happy Fuzzing! and happy to answer any questions regarding the library. We are eagerly looking for feedback to further improve the tool.