docs: add fuzz corpus seeding implementation plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-03-26 00:04:58 +00:00
parent 22be4dd064
commit b0adff2a9b

View file

@ -0,0 +1,662 @@
# Fuzz Corpus Seeding Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Generate valid USB/IP seed files for all 5 fuzz targets to accelerate fuzzer coverage.
**Architecture:** A standalone Rust binary (`lib/fuzz/gen_corpus.rs`) with no external dependencies constructs raw USB/IP protocol bytes and writes them to corpus directories. A nix app compiles and runs it. Raw byte construction is used instead of library types to avoid Cargo workspace complications with the `cargo-fuzz` metadata in `lib/fuzz/Cargo.toml`.
**Tech Stack:** Rust (std only), Nix flake app
---
### Task 1: Create gen_corpus.rs skeleton with helpers
**Files:**
- Create: `lib/fuzz/gen_corpus.rs`
- [ ] **Step 1: Create the file with constants, helpers, and empty main()**
```rust
use std::fs;
// USB/IP protocol constants
const USBIP_VERSION: u16 = 0x0111;
const OP_REQ_DEVLIST: u16 = 0x8005;
const OP_REQ_IMPORT: u16 = 0x8003;
const USBIP_CMD_SUBMIT: u16 = 0x0001;
const USBIP_CMD_UNLINK: u16 = 0x0002;
// Direction
const DIR_OUT: u32 = 0;
const DIR_IN: u32 = 1;
/// Write a seed file to corpus/<target>/seed-<name>
fn write_seed(target: &str, name: &str, data: &[u8]) {
let dir = format!("corpus/{target}");
fs::create_dir_all(&dir).unwrap();
let path = format!("{dir}/seed-{name}");
fs::write(&path, data).unwrap();
println!(" {path} ({} bytes)", data.len());
}
/// OP_REQ_DEVLIST: version(2) + command(2) + status(4) = 8 bytes
fn op_req_devlist() -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&USBIP_VERSION.to_be_bytes());
buf.extend_from_slice(&OP_REQ_DEVLIST.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf
}
/// OP_REQ_IMPORT: version(2) + command(2) + status(4) + busid(32) = 40 bytes
fn op_req_import(busid: &str) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&USBIP_VERSION.to_be_bytes());
buf.extend_from_slice(&OP_REQ_IMPORT.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
let mut busid_bytes = [0u8; 32];
let len = busid.len().min(32);
busid_bytes[..len].copy_from_slice(&busid.as_bytes()[..len]);
buf.extend_from_slice(&busid_bytes);
buf
}
/// USBIP_CMD_SUBMIT: 48-byte header + data (OUT only) + iso descriptors
/// Wire: version(2) + command(2) + seqnum(4) + devid(4) + direction(4) + ep(4)
/// + transfer_flags(4) + transfer_buffer_length(4) + start_frame(4)
/// + number_of_packets(4) + interval(4) + setup(8) + [data] + [iso_desc]
fn cmd_submit(
seqnum: u32,
direction: u32,
ep: u32,
transfer_buffer_length: u32,
number_of_packets: u32,
interval: u32,
setup: [u8; 8],
data: &[u8],
iso_descriptors: &[u8],
) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes()); // version (0 for URB phase)
buf.extend_from_slice(&USBIP_CMD_SUBMIT.to_be_bytes()); // command
buf.extend_from_slice(&seqnum.to_be_bytes()); // seqnum
buf.extend_from_slice(&2u32.to_be_bytes()); // devid
buf.extend_from_slice(&direction.to_be_bytes()); // direction
buf.extend_from_slice(&ep.to_be_bytes()); // ep
buf.extend_from_slice(&0u32.to_be_bytes()); // transfer_flags
buf.extend_from_slice(&transfer_buffer_length.to_be_bytes()); // transfer_buffer_length
buf.extend_from_slice(&0u32.to_be_bytes()); // start_frame
buf.extend_from_slice(&number_of_packets.to_be_bytes()); // number_of_packets
buf.extend_from_slice(&interval.to_be_bytes()); // interval
buf.extend_from_slice(&setup); // setup (8 bytes)
buf.extend_from_slice(data);
buf.extend_from_slice(iso_descriptors);
buf
}
/// USBIP_CMD_UNLINK: 48 bytes total
/// Wire: version(2) + command(2) + seqnum(4) + devid(4) + direction(4) + ep(4)
/// + unlink_seqnum(4) + padding(24)
fn cmd_unlink(seqnum: u32, unlink_seqnum: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&USBIP_CMD_UNLINK.to_be_bytes());
buf.extend_from_slice(&seqnum.to_be_bytes());
buf.extend_from_slice(&2u32.to_be_bytes()); // devid
buf.extend_from_slice(&0u32.to_be_bytes()); // direction
buf.extend_from_slice(&0u32.to_be_bytes()); // ep
buf.extend_from_slice(&unlink_seqnum.to_be_bytes());
buf.extend_from_slice(&[0u8; 24]); // padding
buf
}
/// ISO packet descriptor: offset(4) + length(4) + actual_length(4) + status(4) = 16 bytes
fn iso_desc(offset: u32, length: u32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&offset.to_be_bytes());
buf.extend_from_slice(&length.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes()); // actual_length (client sets to 0)
buf.extend_from_slice(&0u32.to_be_bytes()); // status (client sets to 0)
buf
}
/// Control IN setup: GET_DESCRIPTOR
/// bmRequestType=0x80 (IN, Standard, Device), bRequest=0x06 (GET_DESCRIPTOR)
/// wValue=desc_type<<8|desc_index, wIndex=0, wLength=length
fn setup_get_desc(desc_type: u8, desc_index: u8, length: u16) -> [u8; 8] {
[
0x80, 0x06,
desc_index, desc_type,
0x00, 0x00,
(length & 0xFF) as u8, (length >> 8) as u8,
]
}
/// Control IN setup: GET_DESCRIPTOR with class-specific request type
/// bmRequestType=0x81 (IN, Standard, Interface)
fn setup_get_desc_iface(desc_type: u8, desc_index: u8, iface: u16, length: u16) -> [u8; 8] {
[
0x81, 0x06,
desc_index, desc_type,
(iface & 0xFF) as u8, (iface >> 8) as u8,
(length & 0xFF) as u8, (length >> 8) as u8,
]
}
/// Control OUT setup: SET_CONFIGURATION
fn setup_set_config(config: u16) -> [u8; 8] {
[
0x00, 0x09,
(config & 0xFF) as u8, (config >> 8) as u8,
0x00, 0x00,
0x00, 0x00,
]
}
/// Control OUT setup: SET_INTERFACE (alternate setting)
fn setup_set_interface(iface: u16, alt: u16) -> [u8; 8] {
[
0x01, 0x0B,
(alt & 0xFF) as u8, (alt >> 8) as u8,
(iface & 0xFF) as u8, (iface >> 8) as u8,
0x00, 0x00,
]
}
/// Build a standard USB enumeration sequence (5 CMD_SUBMITs, seqnums 1-5)
fn enumeration_sequence() -> Vec<u8> {
let mut buf = Vec::new();
// 1: GET_DESCRIPTOR(Device, 64 bytes)
buf.extend(cmd_submit(1, DIR_IN, 0, 64, 0, 0, setup_get_desc(1, 0, 64), &[], &[]));
// 2: GET_DESCRIPTOR(Device, 18 bytes)
buf.extend(cmd_submit(2, DIR_IN, 0, 18, 0, 0, setup_get_desc(1, 0, 18), &[], &[]));
// 3: GET_DESCRIPTOR(Configuration, 9 bytes)
buf.extend(cmd_submit(3, DIR_IN, 0, 9, 0, 0, setup_get_desc(2, 0, 9), &[], &[]));
// 4: GET_DESCRIPTOR(Configuration, 255 bytes)
buf.extend(cmd_submit(4, DIR_IN, 0, 255, 0, 0, setup_get_desc(2, 0, 255), &[], &[]));
// 5: SET_CONFIGURATION(1)
buf.extend(cmd_submit(5, DIR_OUT, 0, 0, 0, 0, setup_set_config(1), &[], &[]));
buf
}
fn main() {
println!("Generating fuzz corpus seeds...\n");
// Seed functions will be added in subsequent tasks
println!("\nDone.");
}
```
- [ ] **Step 2: Compile to verify it builds**
Run from repo root:
```bash
rustc lib/fuzz/gen_corpus.rs -o /tmp/gen_corpus && /tmp/gen_corpus
```
Expected output:
```
Generating fuzz corpus seeds...
Done.
```
- [ ] **Step 3: Commit**
```bash
git add lib/fuzz/gen_corpus.rs
git commit -m "feat(fuzz): add gen_corpus.rs skeleton with protocol helpers"
```
---
### Task 2: Add fuzz_parse_command seeds
**Files:**
- Modify: `lib/fuzz/gen_corpus.rs`
- [ ] **Step 1: Add gen_parse_command() and call from main()**
Add this function before `main()`:
```rust
fn gen_parse_command() {
println!("fuzz_parse_command:");
// Valid OP_REQ_DEVLIST (8 bytes)
write_seed("fuzz_parse_command", "devlist", &op_req_devlist());
// Valid OP_REQ_IMPORT with busid "0-0-0" (40 bytes)
write_seed("fuzz_parse_command", "import", &op_req_import("0-0-0"));
}
```
Add to `main()` body (replace the placeholder comment):
```rust
gen_parse_command();
```
- [ ] **Step 2: Compile, run from lib/fuzz/, verify files**
```bash
rustc lib/fuzz/gen_corpus.rs -o /tmp/gen_corpus && (cd lib/fuzz && /tmp/gen_corpus)
```
Expected: Two files created in `lib/fuzz/corpus/fuzz_parse_command/`:
- `seed-devlist` (8 bytes)
- `seed-import` (40 bytes)
Verify with:
```bash
ls -la lib/fuzz/corpus/fuzz_parse_command/seed-*
```
- [ ] **Step 3: Commit**
```bash
git add lib/fuzz/gen_corpus.rs lib/fuzz/corpus/fuzz_parse_command/seed-*
git commit -m "feat(fuzz): add parse_command corpus seeds"
```
---
### Task 3: Add fuzz_urb_hid seeds
**Files:**
- Modify: `lib/fuzz/gen_corpus.rs`
The HID fuzz target uses: ep 0x81 (interrupt IN, 8 bytes, interval 10), ep 0 (control).
HID class requests: GET_DESCRIPTOR(HID Report, type 0x22), SET_IDLE (class OUT 0x0A).
- [ ] **Step 1: Add gen_urb_hid() and call from main()**
Add this function before `main()`:
```rust
fn gen_urb_hid() {
println!("fuzz_urb_hid:");
// Seed 1: Standard enumeration sequence
write_seed("fuzz_urb_hid", "enumerate", &enumeration_sequence());
// Seed 2: Enumeration + HID-specific requests
let mut buf = enumeration_sequence();
let seq = 6;
// GET_DESCRIPTOR(HID Report Descriptor, type=0x22, 128 bytes) on interface 0
buf.extend(cmd_submit(seq, DIR_IN, 0, 128, 0, 0,
setup_get_desc_iface(0x22, 0, 0, 128), &[], &[]));
// SET_IDLE (class request: bmRequestType=0x21, bRequest=0x0A)
buf.extend(cmd_submit(seq + 1, DIR_OUT, 0, 0, 0, 0,
[0x21, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], &[], &[]));
// GET_REPORT (class request: bmRequestType=0xA1, bRequest=0x01, report type=Input(1))
buf.extend(cmd_submit(seq + 2, DIR_IN, 0, 8, 0, 0,
[0xA1, 0x01, 0x00, 0x01, 0x00, 0x00, 0x08, 0x00], &[], &[]));
write_seed("fuzz_urb_hid", "hid-class-requests", &buf);
// Seed 3: Interrupt IN polls on ep 0x81 (keyboard reports)
let mut buf = enumeration_sequence();
for i in 0..4u32 {
buf.extend(cmd_submit(6 + i, DIR_IN, 0x01, 8, 0, 10, [0u8; 8], &[], &[]));
}
write_seed("fuzz_urb_hid", "hid-interrupt-in", &buf);
// Seed 4: CMD_SUBMIT followed by CMD_UNLINK
let mut buf = cmd_submit(1, DIR_IN, 0x01, 8, 0, 10, [0u8; 8], &[], &[]);
buf.extend(cmd_unlink(2, 1)); // unlink seqnum 1
write_seed("fuzz_urb_hid", "unlink", &buf);
// Seed 5: Zero-length control transfer (GET_STATUS)
// bmRequestType=0x80 (IN, Standard, Device), bRequest=0x00 (GET_STATUS)
write_seed("fuzz_urb_hid", "get-status",
&cmd_submit(1, DIR_IN, 0, 2, 0, 0,
[0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00], &[], &[]));
}
```
Add to `main()`:
```rust
gen_urb_hid();
```
- [ ] **Step 2: Compile, run, verify**
```bash
rustc lib/fuzz/gen_corpus.rs -o /tmp/gen_corpus && (cd lib/fuzz && /tmp/gen_corpus)
ls -la lib/fuzz/corpus/fuzz_urb_hid/seed-*
```
Expected: 5 seed files in `lib/fuzz/corpus/fuzz_urb_hid/`.
- [ ] **Step 3: Commit**
```bash
git add lib/fuzz/gen_corpus.rs lib/fuzz/corpus/fuzz_urb_hid/seed-*
git commit -m "feat(fuzz): add urb_hid corpus seeds"
```
---
### Task 4: Add fuzz_urb_cdc seeds
**Files:**
- Modify: `lib/fuzz/gen_corpus.rs`
CDC ACM endpoints: 0x81 (interrupt IN, 8 bytes), 0x82 (bulk IN, 512 bytes), 0x02 (bulk OUT, 512 bytes).
CDC class requests: SET_LINE_CODING (0x20, 7-byte OUT), SET_CONTROL_LINE_STATE (0x22).
- [ ] **Step 1: Add gen_urb_cdc() and call from main()**
Add this function before `main()`:
```rust
fn gen_urb_cdc() {
println!("fuzz_urb_cdc:");
// Seed 1: Standard enumeration
write_seed("fuzz_urb_cdc", "enumerate", &enumeration_sequence());
// Seed 2: Enumeration + CDC class requests
let mut buf = enumeration_sequence();
let seq = 6;
// SET_LINE_CODING: bmRequestType=0x21 (OUT, Class, Interface), bRequest=0x20
// Data: 115200 baud (LE: 0x00 0x01 0xC2 0x00), 1 stop bit, no parity, 8 data bits
let line_coding: [u8; 7] = [0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08];
buf.extend(cmd_submit(seq, DIR_OUT, 0, 7, 0, 0,
[0x21, 0x20, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00], &line_coding, &[]));
// SET_CONTROL_LINE_STATE: DTR + RTS active (wValue=0x0003)
buf.extend(cmd_submit(seq + 1, DIR_OUT, 0, 0, 0, 0,
[0x21, 0x22, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00], &[], &[]));
write_seed("fuzz_urb_cdc", "cdc-class-requests", &buf);
// Seed 3: Bulk OUT data transfer on ep 0x02
let mut buf = enumeration_sequence();
let test_data = b"Hello, CDC!\r\n";
buf.extend(cmd_submit(6, DIR_OUT, 0x02, test_data.len() as u32, 0, 0,
[0u8; 8], test_data, &[]));
write_seed("fuzz_urb_cdc", "cdc-bulk-out", &buf);
// Seed 4: Bulk IN on ep 0x82
let mut buf = enumeration_sequence();
buf.extend(cmd_submit(6, DIR_IN, 0x02, 512, 0, 0, [0u8; 8], &[], &[]));
write_seed("fuzz_urb_cdc", "cdc-bulk-in", &buf);
// Seed 5: Interrupt IN on ep 0x81 (notification)
let mut buf = enumeration_sequence();
buf.extend(cmd_submit(6, DIR_IN, 0x01, 8, 0, 10, [0u8; 8], &[], &[]));
write_seed("fuzz_urb_cdc", "cdc-interrupt-in", &buf);
}
```
Add to `main()`:
```rust
gen_urb_cdc();
```
- [ ] **Step 2: Compile, run, verify**
```bash
rustc lib/fuzz/gen_corpus.rs -o /tmp/gen_corpus && (cd lib/fuzz && /tmp/gen_corpus)
ls -la lib/fuzz/corpus/fuzz_urb_cdc/seed-*
```
Expected: 5 seed files in `lib/fuzz/corpus/fuzz_urb_cdc/`.
- [ ] **Step 3: Commit**
```bash
git add lib/fuzz/gen_corpus.rs lib/fuzz/corpus/fuzz_urb_cdc/seed-*
git commit -m "feat(fuzz): add urb_cdc corpus seeds"
```
---
### Task 5: Add fuzz_urb_uac seeds
**Files:**
- Modify: `lib/fuzz/gen_corpus.rs`
UAC loopback device:
- Interface 0: AudioControl (no endpoints)
- Interface 1: AudioStreaming OUT, ep 0x01 (isochronous adaptive, 192 bytes max)
- Interface 2: AudioStreaming IN, ep 0x82 (isochronous async, 192 bytes max)
- Format: 48kHz 16-bit stereo PCM. Frame = 4 bytes/sample * 48 samples = 192 bytes.
- [ ] **Step 1: Add gen_urb_uac() and call from main()**
Add this function before `main()`:
```rust
fn gen_urb_uac() {
println!("fuzz_urb_uac:");
// Seed 1: Standard enumeration
write_seed("fuzz_urb_uac", "enumerate", &enumeration_sequence());
// Seed 2: Enumeration + SET_INTERFACE to activate audio streaming
let mut buf = enumeration_sequence();
let seq = 6;
// Activate AudioStreaming OUT (interface 1, alt setting 1)
buf.extend(cmd_submit(seq, DIR_OUT, 0, 0, 0, 0,
setup_set_interface(1, 1), &[], &[]));
// Activate AudioStreaming IN (interface 2, alt setting 1)
buf.extend(cmd_submit(seq + 1, DIR_OUT, 0, 0, 0, 0,
setup_set_interface(2, 1), &[], &[]));
write_seed("fuzz_urb_uac", "uac-set-interface", &buf);
// Seed 3: Isochronous OUT on ep 0x01 (playback)
// 1 frame of 48kHz 16-bit stereo = 192 bytes
let audio_frame = vec![0x80u8; 192]; // silence in signed 16-bit
let iso = iso_desc(0, 192);
let mut buf = enumeration_sequence();
// Activate interface first
buf.extend(cmd_submit(6, DIR_OUT, 0, 0, 0, 0, setup_set_interface(1, 1), &[], &[]));
// ISO OUT transfer: 1 packet of 192 bytes
buf.extend(cmd_submit(7, DIR_OUT, 0x01, 192, 1, 1,
[0u8; 8], &audio_frame, &iso));
write_seed("fuzz_urb_uac", "uac-iso-out", &buf);
// Seed 4: Isochronous IN on ep 0x82 (capture)
let iso = iso_desc(0, 192);
let mut buf = enumeration_sequence();
buf.extend(cmd_submit(6, DIR_OUT, 0, 0, 0, 0, setup_set_interface(2, 1), &[], &[]));
// ISO IN transfer: 1 packet, expects 192 bytes back
buf.extend(cmd_submit(7, DIR_IN, 0x02, 192, 1, 1, [0u8; 8], &[], &iso));
write_seed("fuzz_urb_uac", "uac-iso-in", &buf);
// Seed 5: Multi-packet ISO OUT (3 frames)
let audio_data = vec![0x80u8; 576]; // 3 * 192 bytes
let mut iso = Vec::new();
iso.extend(iso_desc(0, 192));
iso.extend(iso_desc(192, 192));
iso.extend(iso_desc(384, 192));
let mut buf = enumeration_sequence();
buf.extend(cmd_submit(6, DIR_OUT, 0, 0, 0, 0, setup_set_interface(1, 1), &[], &[]));
buf.extend(cmd_submit(7, DIR_OUT, 0x01, 576, 3, 1,
[0u8; 8], &audio_data, &iso));
write_seed("fuzz_urb_uac", "uac-iso-out-multi", &buf);
}
```
Add to `main()`:
```rust
gen_urb_uac();
```
- [ ] **Step 2: Compile, run, verify**
```bash
rustc lib/fuzz/gen_corpus.rs -o /tmp/gen_corpus && (cd lib/fuzz && /tmp/gen_corpus)
ls -la lib/fuzz/corpus/fuzz_urb_uac/seed-*
```
Expected: 5 seed files in `lib/fuzz/corpus/fuzz_urb_uac/`.
- [ ] **Step 3: Commit**
```bash
git add lib/fuzz/gen_corpus.rs lib/fuzz/corpus/fuzz_urb_uac/seed-*
git commit -m "feat(fuzz): add urb_uac corpus seeds with ISO packets"
```
---
### Task 6: Add fuzz_handle_client seeds
**Files:**
- Modify: `lib/fuzz/gen_corpus.rs`
The `fuzz_handle_client` target runs the full `handler()` flow: negotiation then URB loop.
The simulated device has busid `"0-0-0"` (from `UsbDevice::new(0)` in `lib/src/device.rs:67`).
It uses a HID keyboard device.
- [ ] **Step 1: Add gen_handle_client() and call from main()**
Add this function before `main()`:
```rust
fn gen_handle_client() {
println!("fuzz_handle_client:");
// Seed 1: OP_REQ_DEVLIST only (tests early negotiation path)
write_seed("fuzz_handle_client", "devlist-only", &op_req_devlist());
// Seed 2: OP_REQ_DEVLIST then OP_REQ_IMPORT (two-step negotiation)
let mut buf = op_req_devlist();
buf.extend(op_req_import("0-0-0"));
write_seed("fuzz_handle_client", "devlist-then-import", &buf);
// Seed 3: OP_REQ_IMPORT + enumeration sequence
let mut buf = op_req_import("0-0-0");
buf.extend(enumeration_sequence());
write_seed("fuzz_handle_client", "import-enumerate", &buf);
// Seed 4: Full session — import + enumerate + HID class requests + interrupt
let mut buf = op_req_import("0-0-0");
buf.extend(enumeration_sequence());
let seq = 6;
// HID Report Descriptor
buf.extend(cmd_submit(seq, DIR_IN, 0, 128, 0, 0,
setup_get_desc_iface(0x22, 0, 0, 128), &[], &[]));
// SET_IDLE
buf.extend(cmd_submit(seq + 1, DIR_OUT, 0, 0, 0, 0,
[0x21, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], &[], &[]));
// Interrupt IN polls
for i in 0..4u32 {
buf.extend(cmd_submit(seq + 2 + i, DIR_IN, 0x01, 8, 0, 10, [0u8; 8], &[], &[]));
}
write_seed("fuzz_handle_client", "import-hid-full", &buf);
}
```
Add to `main()`:
```rust
gen_handle_client();
```
- [ ] **Step 2: Compile, run, verify**
```bash
rustc lib/fuzz/gen_corpus.rs -o /tmp/gen_corpus && (cd lib/fuzz && /tmp/gen_corpus)
ls -la lib/fuzz/corpus/fuzz_handle_client/seed-*
```
Expected: 4 seed files in `lib/fuzz/corpus/fuzz_handle_client/`.
- [ ] **Step 3: Commit**
```bash
git add lib/fuzz/gen_corpus.rs lib/fuzz/corpus/fuzz_handle_client/seed-*
git commit -m "feat(fuzz): add handle_client corpus seeds"
```
---
### Task 7: Add gen-fuzz-corpus nix app
**Files:**
- Modify: `flake.nix`
- [ ] **Step 1: Add the gen-fuzz-corpus app to flake.nix**
In the `apps` `let` block (after the `fuzz-clean-usbip` definition, before `in {`), add:
```nix
gen-fuzz-corpus = pkgs.writeShellScriptBin "gen-fuzz-corpus" ''
set -euo pipefail
root="$(${pkgs.git}/bin/git rev-parse --show-toplevel)"
${pkgs.rustc}/bin/rustc "$root/lib/fuzz/gen_corpus.rs" -o /tmp/gen-fuzz-corpus
cd "$root/lib/fuzz"
/tmp/gen-fuzz-corpus
'';
```
In the `in { ... }` block that defines the app outputs, add alongside the existing apps:
```nix
gen-fuzz-corpus = {
type = "app";
program = "${gen-fuzz-corpus}/bin/gen-fuzz-corpus";
};
```
- [ ] **Step 2: Verify nix app runs**
```bash
nix run .#gen-fuzz-corpus
```
Expected: All seed files generated with summary printed.
- [ ] **Step 3: Commit**
```bash
git add flake.nix
git commit -m "feat(fuzz): add gen-fuzz-corpus nix app"
```
---
### Task 8: Final verification
- [ ] **Step 1: Run the generator and count seed files**
```bash
nix run .#gen-fuzz-corpus
```
Then verify all expected seeds exist:
```bash
echo "=== fuzz_parse_command ===" && ls lib/fuzz/corpus/fuzz_parse_command/seed-*
echo "=== fuzz_urb_hid ===" && ls lib/fuzz/corpus/fuzz_urb_hid/seed-*
echo "=== fuzz_urb_cdc ===" && ls lib/fuzz/corpus/fuzz_urb_cdc/seed-*
echo "=== fuzz_urb_uac ===" && ls lib/fuzz/corpus/fuzz_urb_uac/seed-*
echo "=== fuzz_handle_client ===" && ls lib/fuzz/corpus/fuzz_handle_client/seed-*
```
Expected totals:
- `fuzz_parse_command`: 2 seeds
- `fuzz_urb_hid`: 5 seeds
- `fuzz_urb_cdc`: 5 seeds
- `fuzz_urb_uac`: 5 seeds
- `fuzz_handle_client`: 4 seeds
Total: 21 seed files.
- [ ] **Step 2: Spot-check a seed with hexdump**
```bash
xxd lib/fuzz/corpus/fuzz_parse_command/seed-devlist
```
Expected: `0111 8005 0000 0000` (8 bytes: version 0x0111, command 0x8005, status 0).
- [ ] **Step 3: Commit all generated seed files**
```bash
git add lib/fuzz/corpus/*/seed-*
git commit -m "feat(fuzz): add generated corpus seed files"
```