Canfuzz: Introducing the Canister Fuzzing Framework

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:

  1. LibAFL: The industry standard for building scalable, coverage-guided fuzzers.
  2. 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,

  1. The fuzzer generates an input (e.g., a byte array).
  2. It spins up a PocketIC instance and sends the input to your canister.
  3. The instrumented Wasm reports back which lines of code were executed.
  4. If a new code path is found, LibAFL saves that input and mutates it further to explore deeper.
  5. 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

Happy Fuzzing! and happy to answer any questions regarding the library. We are eagerly looking for feedback to further improve the tool.

20 Likes

From what I understand about the fuzzing process is you run the program for a long time to change bits and inputs in the code, is this not very cost heavy on canisters ? Would love to understand more about this feature

This is a great development by the way, kudos

Fuzzing is a common process for rapidly abusing a target by cramming inputs in a way that isn’t normally expected, and it’s a normal part of security testing at scale before prod (sometimes smoke/load Env). In this case I’d be more interested to try first in dfx, but for most interesting dapps 1 canister isn’t enough, therefore, testing prod should be necessary and minimally impactful. At Omnity we use 3+ subnets consistently, so although I’d love to automate it I have to schedule a fuzzer vs cycles staged by my architect and product owners. I’m looking forward to any comments here about running canisters individually in dfx first with a generic fuzzer, then with pocketIC + canfuzz. I’m very glad to see this offered instead of DIY

See you on Spaces Friday :clinking_beer_mugs:

5 Likes

This is great! I’m looking forward to using this. It’ll be very useful for something I’m working on currently actually. Thank you

3 Likes

Since the fuzzing is expected to be done locally with PocketIC, cost heavy / cycles shouldn’t be an issue since it can be fabricated on the fly. We don’t recommend fuzzing on an already deployed mainnet canisters as that would cause significant cycles cost.

2 Likes

DFX under the hood uses pocketIC and it supports multiple subnets, so it shouldn’t be an issue to port over the setup to canfuzz.

You are not limited to one canister, the pocketIC setup can be initialized with multiple canisters. The only limitation comes from that fuzzing engine depends on coverage data from a single canister. This can change in the future, if there is more interest for such a usecase.

A new version of canfuzz (v0.4.0) was just released.

This version contains a new Candid aware mutator which generates (most of the time) new inputs that are valid candid encoding of the canister method that’s being fuzzed.

To take advantage of this, you need implement the get_candid_args method in the FuzzerOrchestrator trait. You can find an example implementation here (1, 2)

4 Likes

Mr. Sekar,

This is a very useful tool for the whole IC ecosystem, we were lacking a fuzzing tool to do thorough automated canister testing and now we have one.

For those in the thread asking questions like @bitel911:

This tool is not meant to be used with a running canister on the IC network already, but instead for code that you are writing and want to thoroughly test using this fuzzing tool.

Fuzzing tools check your code in the places where you get inputs, to make sure it does not break, they do this automatically after you do the corresponding setup.

In short this is the sequence you would follow:

  • Create a fuzzer canister using this code
  • Add the WASM code of your canister that you want to test into the fuzzer configuation
  • Run the fuzzer automated tests
  • Inspect what happened, and fixed any errors or crashes.

It would be great to get a full walkthrough tutorial, but in the meantime this tells you the high level setup effort.

1 Like

A new version of canfuzz (v0.5.0) was just released.

This version mainly includes support for wasm64 so that the fuzzers are fully compatible with Motoko canisters. Other changes include helper APIs to reduce repeating boilerplate code.

Happy fuzzing!

1 Like