.github/skills/fuzzing/SKILL.md
Run, optimize, and debug OpenVMM fuzzers. Covers cargo-fuzz targets, crash reproduction, lldb debugging, code coverage analysis, entropy optimization, and multi-target parallel campaigns.
npx skillsauth add microsoft/openvmm fuzzingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
rustup toolchain install nightlycargo install cargo-fuzzsudo apt-get install -y lldbRUSTFLAGS="-Ctarget-feature=+lse,+neon" or builds fail with atomics errors. Not needed on x86_64.Fuzz targets live in <crate>/fuzz/ directories alongside the crate they test.
Each has a Cargo.toml with cargo-fuzz = true metadata and a fuzz_<name>.rs
binary. Find all targets:
cargo xtask fuzz list
# Continuous fuzzing (finds new crashes)
cargo +nightly xtask fuzz run fuzz_ide
# Reproduce a specific crash artifact
cargo +nightly xtask fuzz run fuzz_ide path/to/crash-artifact
The xtask wrapper sets XTASK_FUZZ_REPRO=1 automatically when an artifact
path is provided, which enables init_tracing_if_repro() in the fuzz target.
cargo +nightly xtask fuzz build fuzz_ide
The binary lands at target/<triple>/release/fuzz_ide (e.g.,
target/aarch64-unknown-linux-gnu/release/fuzz_ide or
target/x86_64-unknown-linux-gnu/release/fuzz_ide).
Running the binary directly is faster for iteration than going through xtask:
# Without tracing (fast, quiet)
./target/<triple>/release/fuzz_ide path/to/crash-artifact
# With tracing (verbose — shows device state at each poll)
XTASK_FUZZ_REPRO=1 ./target/<triple>/release/fuzz_ide path/to/crash-artifact
# With backtrace
RUST_BACKTRACE=full ./target/<triple>/release/fuzz_ide path/to/crash-artifact
lldb -b -s <file>.auto-confirm true — prevents lldb from blocking on "Do you really
want to quit?" prompts.panic_fmt — rust_begin_unwind is often mangled and
won't resolve. Use -r panic_fmt instead.XTASK_FUZZ_REPRO when using lldb — the tracing output from
repeated polls produces megabytes of spam that buries the debugger output.frame variable may show
nothing. For richer inspection, build with --dev (see below).Create a file (e.g., scratch/lldb_fuzz.cmd):
settings set auto-confirm true
breakpoint set -r panic_fmt
run
bt 20
frame select 1
source list
frame select 2
source list
frame variable
quit
lldb -b -s scratch/lldb_fuzz.cmd \
-- ./target/<triple>/release/fuzz_ide \
path/to/crash-artifact 2>&1 | tail -80
The | tail -80 trims libfuzzer startup noise. Adjust as needed.
rust-lldb adds Rust pretty-printers for Vec, String, etc. It requires
lldb to be installed. Usage is the same but replace lldb with rust-lldb:
rust-lldb -b -s scratch/lldb_fuzz.cmd \
-- ./target/<triple>/release/fuzz_ide \
path/to/crash-artifact 2>&1 | tail -80
Release builds have limited frame variable output. For full variable
inspection, build with --dev:
cargo +nightly xtask fuzz build fuzz_ide -- --dev
The debug binary lands at target/<triple>/debug/fuzz_ide. It's slower but
lldb can inspect all locals and struct fields.
Reaching the actual data through Arc, Mutex, UnsafeCell etc. requires
knowing the field path. Use type lookup to discover struct layouts:
(lldb) type lookup closeable_mutex::CloseableMutex<ide::IdeDevice>
Common wrapper-type traversal patterns:
| Rust type | lldb field path |
|-----------|----------------|
| Arc<T> | var.ptr.pointer->data (through ArcInner) |
| CloseableMutex<T> | .value.value (UnsafeCell inside Mutex) |
| UnsafeCell<T> | .value |
| Option<T> | .$variants$.$variant$.value.__0 (check $discr$ — 0 = None for most types) |
| Rust enum | .$variants$.$variant$N.value.__0 (N = variant index) |
Full path example for IDE device state through Arc<CloseableMutex<IdeDevice>>:
(lldb) expr ide_device.ptr.pointer->data.value.value.channels[0].state
(lldb) expr ide_device.ptr.pointer->data.value.value.channels[0].enlightened_write
(lldb) expr ide_device.ptr.pointer->data.value.value.channels[0].bus_master_state
For drive registers inside Option<DiskDrive> containing an enum variant:
(lldb) expr ide_device.ptr.pointer->data.value.value.channels[0].drives[0].$variants$.$variant$.value.__0.$variants$.$variant$.value.__0.state.regs
To skip the first N hits of a breakpoint (useful when a function is called hundreds of times during polling):
(lldb) breakpoint set -f lib.rs -l 1253
(lldb) breakpoint modify -i 500 1
(lldb) run
This skips the first 500 hits, then stops on hit 501.
Status register values appear as (__0 = 'X') where 'X' is the char at
that byte value. Decode manually: 0x58 = bits 3,4,6 set = drq, dsc,
drdy.
cargo +nightly xtask fuzz tmin fuzz_ide path/to/crash-artifact
# Minimize the corpus (remove redundant inputs)
cargo +nightly xtask fuzz cmin fuzz_ide
Corpus files live in <crate>/fuzz/corpus/<target>/.
Crash artifacts land in <crate>/fuzz/artifacts/<target>/.
Every byte of fuzzer input should drive meaningful behavior. Common wastes:
u.ratio() calls for branching — consumes entropy for each decision. Use Arbitrary enums instead (1 byte per decision vs 8+ for ratio).arbitrary_data() calls — generate composite values with one call instead of field-by-field. E.g., a Guid from [u8; 16] instead of 11 separate calls.0..=N ranges.-fork=N for parallel fuzzing across CPUs. Allocate cores proportionally to fuzzer complexity.Run multiple fuzzers across available CPUs for extended campaigns:
# 6-hour campaign across 112 cores, 7 fuzzers
# Each runs with -fork=N, survives crashes/timeouts/OOMs
nohup bash -c 'RUSTFLAGS="-Ctarget-feature=+lse,+neon" \
cargo +nightly xtask fuzz run fuzz_storvsp -- -- \
-fork=20 -max_total_time=21600 \
-ignore_crashes=1 -ignore_timeouts=1 -ignore_ooms=1 \
-print_final_stats=1' > /tmp/fuzz_storvsp.log 2>&1 &
Allocate cores based on fuzzer complexity:
Monitor progress: tail -1 /tmp/fuzz_*.log
After the run, collect coverage on each fuzzer for gap analysis.
Everything from the main prerequisites, plus:
rustup +nightly component add llvm-toolssudo apt-get install -y lcovThe xtask has built-in coverage support. It runs each corpus entry through a coverage-instrumented build and merges the results:
# Collect coverage data only
cargo +nightly xtask fuzz coverage fuzz_ide
# Collect + generate HTML report (requires lcov + genhtml)
cargo +nightly xtask fuzz coverage fuzz_ide --with-html-report
# Skip rebuild, just regenerate report from existing profdata
cargo +nightly xtask fuzz coverage fuzz_ide --with-html-report --only-report
On aarch64, set RUSTFLAGS="-Ctarget-feature=+lse,+neon" as usual.
Coverage artifacts land in:
| Artifact | Path |
|----------|------|
| profdata | <crate>/fuzz/coverage/<target>/coverage.profdata |
| HTML report | target/<triple>/coverage/<triple>/release/lcov_html_<target>/index.html |
The raw HTML report covers all compiled code including dependencies — the overall percentage is meaningless (typically 4–6%). What matters is coverage of the target crate itself and its immediate domain dependencies.
Use llvm-cov report with source path filtering:
# Find the llvm-cov binary from nightly toolchain
LLVM_COV=$(find $(rustc +nightly --print sysroot) -name "llvm-cov" -type f | head -1)
# Full per-file report, excluding third-party code
$LLVM_COV report \
-instr-profile <crate>/fuzz/coverage/<target>/coverage.profdata \
-object target/<triple>/coverage/<triple>/release/<target> \
--ignore-filename-regex='(rustc|\.cargo|registry)' 2>&1 \
| grep 'vm/devices/storage/ide/src/'
For lcov-based per-file analysis, export to lcov and extract per-file stats:
$LLVM_COV export \
-instr-profile <crate>/fuzz/coverage/<target>/coverage.profdata \
-object target/<triple>/coverage/<triple>/release/<target> \
--ignore-filename-regex='(rustc|\.cargo|registry)' \
-format=lcov > /tmp/coverage.lcov
# Per-file function/line summary from lcov
awk '/SF:.*ide\/src\/lib\.rs$/,/end_of_record/' /tmp/coverage.lcov \
| grep -E '^(FNF|FNH|LF|LH):'
# FNF/FNH = functions found/hit, LF/LH = lines found/hit
# Find uncovered line numbers
awk '/SF:.*ide\/src\/lib\.rs$/,/end_of_record/' /tmp/coverage.lcov \
| grep '^DA:' | grep ',0$' | cut -d: -f2 | cut -d, -f1
Each fuzzer has a primary scope (the crate it exists to test) and secondary scope (dependencies it exercises incidentally). Coverage improvements should focus on primary scope first.
| Fuzzer | Primary scope | Secondary scope | Out of scope |
|--------|--------------|-----------------|--------------|
| fuzz_ide | ide/src/ | scsidisk/src/scsidvd/, scsidisk/src/atapi_scsi.rs, disk_backend/, disklayer_ram/ | pci_core/, vmcore/, guestmem/ (infrastructure) |
| fuzz_storvsp | storvsp/src/ | scsidisk/src/lib.rs, vmbus_ring/, vmbus_async/, disk_backend/ | vmbus_channel/, vmbus_core/ (infrastructure) |
| fuzz_nvme_driver | nvme_driver/src/ | nvme/src/, nvme_spec/, user_driver/ | page_pool_alloc/, vmcore/ |
| fuzz_firmware_uefi_nvram | firmware_uefi/src/service/nvram/ | uefi_nvram_specvars/ | crypto, openssl (infrastructure) |
| fuzz_firmware_uefi_diagnostics | firmware_uefi/src/service/diagnostics/ | — | guestmem/ |
| fuzz_chipset_cmos_rtc | chipset/src/cmos_rtc/ | chipset_device_fuzz/ | pal_async/, vmcore/ |
| fuzz_scsi_buffers | scsi_buffers/src/ | — | guestmem/ |
| fuzz_chipset_battery | chipset/src/battery/ | chipset_device_fuzz/ | — |
Rules:
Primary scope is the goal. Line coverage of the target crate is the metric to optimize. The fuzzer exists to find bugs in this code.
Secondary scope matters when it's a trust boundary. scsidisk parsing
guest-supplied SCSI commands is a real attack surface — coverage there has
value. But disklayer_ram just storing bytes is infrastructure.
Out-of-scope code is noise. pci_core, vmcore, guestmem get
exercised incidentally. Don't optimize the fuzzer to improve their coverage
— those crates deserve their own fuzzers if coverage matters.
Save/restore and resolver code is structurally unreachable. Functions
like save_restore.rs, resolver.rs won't be hit by fuzzing because
the fuzzer doesn't exercise serialization or resource resolution paths.
Don't count them against coverage targets.
Inspect (InspectMut) implementations are low-value. These debug/
diagnostics paths aren't part of the attack surface. Low coverage is
acceptable.
# Run coverage
cargo +nightly xtask fuzz coverage <target> --with-html-report
# Open the HTML report for visual inspection
# target/<triple>/coverage/<triple>/release/lcov_html_<target>/index.html
Look at the HTML report or extract uncovered lines:
awk '/SF:.*<crate>\/src\/lib\.rs$/,/end_of_record/' /tmp/coverage.lcov \
| grep '^DA:' | grep ',0$' | cut -d: -f2 | cut -d, -f1
Read the source at those lines. Classify each uncovered region:
| Classification | Action |
|---------------|--------|
| Reachable via fuzzer input — the fuzzer could reach this code but hasn't found the right input sequence | Investigate why. Is the path gated by specific byte patterns the fuzzer struggles to generate? |
| Structurally unreachable — the fuzzer harness doesn't wire up the code path (e.g., save/restore, PCI config reads the harness skips) | Extend the harness: add new FuzzAction variants, expose more device interfaces to the fuzzer. |
| Error-handling / defensive code — unreachable!(), error paths for invalid hardware states | Low priority. These are worth testing but don't indicate a fuzzer deficiency. |
| Inspect / debug only — InspectMut, Display, Debug impls | Skip entirely. Not attack surface. |
Improvements fall into two categories, in priority order:
A. Extend the fuzz harness
If an entire code path is structurally unreachable, the harness needs changes. Examples:
FuzzAction enum in chipset_device_fuzzB. Improve Arbitrary implementations
If the fuzzer generates mostly one type of operation, check the Arbitrary
impl. Common issues:
After making changes:
cargo +nightly xtask fuzz cmin <target>Track coverage changes over time. Ratchet up: if a change reduces primary scope coverage, investigate why.
Coverage-guided improvement is iterative. Each cycle:
Target ≥85% line coverage in primary scope as a practical goal. The remaining ~15% is typically error-handling code, debug impls, and paths requiring specific hardware/timing conditions the fuzzer can't easily produce.
tools
Investigate CI failures on OpenVMM PRs. Load when a PR has failing CI checks, you need to download and analyze test artifacts, or you need to diagnose build, fmt, clippy, or VMM test failures.
tools
Maintain the OpenVMM Guide and its code-sync mapping. Load when: (1) adding, removing, or moving Guide pages, (2) adding new device crates or CLI args that need doc coverage, (3) updating the doc-code-sync mapping table, or (4) auditing Guide freshness against code changes.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.