Skip to content

Snapchange - Snapshot-Based Fuzzing with KVM Cheatsheet

Snapchange - Snapshot-Based Fuzzing with KVM Cheatsheet

Snapchange (by AWS) is a Rust framework for snapshot-based fuzzing. Traditional fuzzers restart the target for each input, which is fatally slow when the interesting code sits behind expensive setup — a network handshake, a login, a parsed config, a loaded database. Snapchange instead takes a memory snapshot of the target at exactly the moment of interest, then restores that state and mutates from there using KVM, achieving very high iteration rates against code that conventional fuzzing struggles to reach at all.

Snapshot fuzzing requires KVM and root. Run in an isolated environment.

Requirements

RequirementNote
Linux with KVM/dev/kvm accessible
Rust toolchainSnapchange is a Rust library
QEMUUsed to take the snapshot
RootFor KVM and memory access

The Model

PhaseWhat happens
1. SnapshotRun the target in QEMU to the point of interest, dump memory + registers
2. HarnessWrite a Rust fuzzer describing where input goes and when to stop
3. FuzzSnapchange restores the snapshot per iteration and mutates the input
4. TriageCrashes are recorded with the exact register/memory state

The critical insight: all the setup cost is paid once, in the snapshot. Every subsequent iteration starts from that state in microseconds.

Taking a Snapshot

# Conceptual: run the target under QEMU, break at the function of interest,
# then dump physical memory and register state
./snapchange/qemu_snapshot/take_snapshot.sh --target ./my-server
ArtifactContains
fuzzvm.physmemFull guest physical memory
fuzzvm.qemuregsCPU register state
*.symbolsSymbol table for coverage/breakpoints
vmlinux / binaryFor symbolization

Writing a Fuzzer

// Conceptual shape — see Snapchange examples for complete code
impl Fuzzer for MyFuzzer {
    type Input = Vec<u8>;
    const START_ADDRESS: u64 = 0x555555555000;
    const MAX_INPUT_LENGTH: usize = 1024;

    fn set_input(&mut self, input: &Self::Input, fuzzvm: &mut FuzzVm<Self>) -> Result<()> {
        // Write the mutated input into guest memory where the target reads it
        fuzzvm.write_bytes_dirty(VirtAddr(BUFFER_ADDR), CR3, input)?;
        Ok(())
    }

    fn reset_breakpoints(&self) -> Option<&[AddressLookup]> { /* stop conditions */ }
    fn crash_breakpoints(&self) -> Option<&[AddressLookup]> { /* crash sites */ }
}
ElementPurpose
START_ADDRESSWhere execution resumes each iteration
set_inputInject the fuzz input into guest memory
reset_breakpointsWhere an iteration ends normally
crash_breakpointsAddresses that indicate a crash (e.g. panic, abort)

Running

CommandDescription
cargo run -r -- fuzz -c 8Fuzz with 8 cores
cargo run -r -- project translateSymbolize/inspect the snapshot
cargo run -r -- coverageGenerate coverage from the corpus
cargo run -r -- trace <input>Single-step trace an input
cargo run -r -- minimize <input>Shrink a crashing input

Why Snapshot Fuzzing Wins Here

Target characteristicConventional fuzzingSnapchange
Expensive startupPays it every iterationPays it once
Requires auth/handshakeHard to reachSnapshot past it
Stateful protocolDifficultSnapshot mid-session
Kernel/hypervisor codeVery hardNatural fit

Coverage & Triage

CapabilityNote
Breakpoint coverageCoverage via breakpoints on basic blocks
Crash dedupGrouped by faulting address/state
Single-step tracesFull instruction trace for a given input
Memory inspectionRead guest memory at crash time

Snapchange vs Other Fuzzers

AspectSnapchangeAFL++LibAFL (Nyx)syzkaller
ExecutionKVM snapshot restoreProcess forkSnapshot (Nyx)VM + syscalls
Setup costPaid onceEvery iterationOncePer VM
TargetAnything in a VM snapshotUserspace binariesConfigurableOS kernels
EffortHigh (write a Rust fuzzer)LowHighMedium

Compare with LibAFL (which also offers snapshot backends) and syzkaller for kernel syscall fuzzing.

Resources