When a real USB device stalls an endpoint, the VM sends
CLEAR_FEATURE(ENDPOINT_HALT) to recover. Previously this was a no-op,
leaving the endpoint permanently stalled and causing the guest to hang.
Now the CLEAR_FEATURE handler in device.rs finds the interface that owns
the stalled endpoint and calls clear_halt on its handler. For host
passthrough this forwards to nusb's new Interface::clear_halt; for
simulated devices it remains a no-op.
Fixes YubiKey (and other CCID devices) hanging after endpoint stall
during passthrough.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The client was passing devid=0 to vhci_hcd during attach, but the
server computes expected_devid from (bus_num << 16) | dev_num. This
caused the server's devid validation (added in 76f5134) to reject
every URB from the kernel client.
Add a second fuzzing engine alongside the existing libFuzzer/cargo-fuzz
setup. AFL++ runs with persistent mode (afl::fuzz! macro), LLVM plugins
(CmpLog, IJON), and a SymCC concolic companion for hybrid fuzzing.
- cargo-afl built from afl.rs with a patch for CARGO_AFL_DIR /
CARGO_AFL_LLVM_DIR env-var overrides
- AFL++ built with LLVM 22 plugins to match rust-nightly
- Persistent-mode fuzz targets in lib/fuzz-afl/
- --jobs N parallel fuzzing: main instance in foreground, secondaries
and SymCC companion as systemd transient units in a slice
- Ctrl+c / exit cleans up all background processes via slice stop
- AFL_AUTORESUME=1 for clean restarts after previous runs
- fuzz-clean-afl collects crashes from all instance directories
- Shared harness logic in lib/src/fuzz_harness.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The kernel's valid_request() in stub_rx.c tears down the TCP connection
when devid doesn't match (SDEV_EVENT_ERROR_TCP). Previously we sent an
error response and continued, which is non-standard. Now we break out
of the loop to close the connection, matching the kernel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CMD_UNLINK handler returned -EINVAL for devid mismatch and -ENOENT
for not-found URBs, but the Linux kernel's stub_recv_cmd_unlink() returns
status 0 in both cases. Non-standard status codes could confuse the host
kernel's vhci_hcd driver. Fixes 64 fuzzer crash artifacts across
fuzz_urb_hid and fuzz_urb_cdc targets.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace debug_assert!(status == 0) with proper error returns. Per the
USB/IP protocol spec, the status field in these requests is "unused,
shall be set to 0" — a non-zero value indicates a non-compliant client
and should be rejected at the parsing boundary.
Also document fuzzer crash triage guidelines in CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds `nix run .#fuzz-clean-usbip -- <target>` to re-check crash/oom/timeout
artifacts and delete the ones that no longer reproduce (already fixed).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Values are intentionally USB-spec-aligned (0x00/0x80), not rusb-aligned.
Safe since the enum is only pattern-matched, never cast to integer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spec reviewer caught three missing items that would cause build failures:
- pub use rusb::Direction needs local replacement enum
- rusb::Version conversion impls in device.rs need removal
- stale rusb::Direction comment in usbip_protocol.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous TOCTOU fix prevented sending both responses from two
threads, but a channel-ordering problem remained: the spawn_blocking
task and the UNLINK handler both sent to the same mpsc channel from
different threads, so RET_UNLINK could arrive at the kernel before
RET_SUBMIT. The kernel (vhci_rx.c) then gives back the URB via the
unlink path, and the subsequent RET_SUBMIT triggers "cannot find a
urb" → VDEV_EVENT_ERROR_TCP → device disconnect.
Fix by making the spawn_blocking task the sole sender of RET_UNLINK
when the URB is still in-flight. The UNLINK handler now stores its
response header in the InFlightUrb entry instead of sending
immediately. The spawn_blocking task drains this after sending
RET_SUBMIT (or instead of it, if cancelled), guaranteeing same-thread
FIFO ordering through the channel. The UNLINK handler only sends
RET_UNLINK directly for the "not found" case, where the entry has
already been removed — meaning RET_SUBMIT is already in the channel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The spawn_blocking task checked is_cancelled() before removing the URB
from in_flight, allowing the UNLINK handler to find the entry, cancel
the token, and send RET_UNLINK(status=0) in the gap — while the task
proceeded to also send RET_SUBMIT. Both responses for the same URB is
a fatal protocol violation: if the kernel receives RET_UNLINK first it
gives back the URB, then RET_SUBMIT can't find it and disconnects.
Fix by removing from in_flight before checking is_cancelled(), making
the UNLINK handler and completion path mutually exclusive on the map.
Also downgrade "not found in-flight" to trace — it's a normal race for
isochronous transfers, not an error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ISO packet descriptors in this kernel's USB/IP implementation use
big-endian encoding (consistent with the rest of the protocol header),
not little-endian as assumed from older kernel source. The LE change
caused length=192 to be read as 0xC0000000, immediately failing the
bounds check on the first isochronous URB.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes for the UAC1 loopback test device:
- Add CS_ENDPOINT descriptors (type 0x25) after audio streaming isochronous
endpoints. The Linux snd-usb-audio driver requires these to recognize the
device. Added class_specific_descriptor field to UsbEndpoint (mirroring the
existing field on UsbInterface) and emit it in the config descriptor builder.
- Remove the 30s URB read timeout from handle_urb_loop. The connection lifetime
is managed by the kernel (vhci_hcd closes the socket on device detach). An
application-level timeout killed healthy idle devices. Fixed tests to properly
shutdown() write halves instead of relying on the timeout.
- Fix ISO packet descriptor byte order from big-endian to little-endian. The
USB/IP protocol uses big-endian for header fields but little-endian for ISO
descriptors (matching the kernel's usbip_pack_iso using cpu_to_le32). With
big-endian, field values like length=192 were byte-swapped to ~3GB, corrupting
isochronous audio streams.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a simulated USB Audio Class 1 loopback device for testing
isochronous transfers. Audio sent to the playback OUT endpoint
(48kHz/16-bit/stereo) is looped back to the capture IN endpoint.
- Add UsbEndpoint::transfer_type() masking bmAttributes to bits 0-1,
fixing dispatch for isochronous endpoints with sync-type sub-bits
- Update all endpoint attribute dispatch sites across the library
- Add UacLoopbackBuffer, UacControlHandler, UacStreamOutHandler,
UacStreamInHandler in lib/src/uac.rs
- Add build_uac_loopback_device() builder function
- Add `test_uac connect` CLI subcommand
- Add 10 unit tests covering buffer, descriptors, and handler behavior
- Add design spec and implementation plan docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace string-based USB error classification with ErrorKind matching:
nusb TransferError is now preserved through io::Error instead of being
destroyed by format!(). Stall→ConnectionReset→EPIPE, Cancelled→
Interrupted→ENOENT, Disconnected→ConnectionAborted→ESHUTDOWN.
- Replace fragile string matching in interrupt IN retry loop with direct
TransferError::Cancelled pattern match.
- Replace 21 production Mutex::lock().unwrap() calls with
.unwrap_or_else(|e| e.into_inner()) to recover from mutex poisoning
instead of cascading panics across the server.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spec conformance:
- Add missing standard control requests for simulated devices:
GetConfiguration, GetStatus (device/interface/endpoint),
ClearFeature, SetFeature, SetAddress
- Replace debug_assert with truncate for path/bus_id wire format
to prevent protocol desync in release builds
Reliability:
- server() now returns Result instead of panicking on bind failure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Critical fixes:
- Validate endpoint number is 0-15 (kernel parity: stub_rx.c)
- Cap in-flight URBs at 256 to prevent DoS resource exhaustion
- Replace expect() with graceful handling on lock contention in find_ep
- Use validated transfer_buffer_length for ISO allocation instead of
unchecked multiplication of client-supplied values
High-priority fixes:
- Validate devid matches imported device in CMD_SUBMIT and CMD_UNLINK
- Fix string descriptor bLength u8 overflow for long strings (>126 chars)
- Use saturating_add for ISO actual_length sum, capped at transfer_buffer_length
- Truncate IN response data exceeding transfer_buffer_length
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Support connecting to guests via Firecracker/Cloud-Hypervisor's Unix
domain socket vsock proxy. The host opens the VMM's socket file,
performs a CONNECT/OK text handshake, then uses the stream for USB/IP.
New address format: fc-vsock:/path/to/socket:<port>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update nusb to c1380673 which allows multiple IsoEndpoint instances per
address, enabling concurrent URB submission from separate threads.
Change UsbInterfaceHandler trait methods from &mut self to &self and
replace Arc<Mutex<Box<dyn Handler>>> with Arc<dyn Handler>. This
removes the serialization bottleneck where the handler mutex was held
for the entire USB transfer duration, causing ISO audio to play at
~67% speed.
Handlers needing interior mutability (HID, CDC) now use Mutex on
individual fields. Passthrough handlers already used Arc<Mutex<>>
internally and need no changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
read timeout while URBs are in-flight
- Interrupt IN transfers now retry with 1s intervals (up to 5 min)
instead of returning an error on timeout. Previously, nusb's 1s
timeout caused a cancelled transfer error (-ENOENT) which told the
kernel the endpoint was intentionally shut down, killing HID.
- Release the nusb Interface Mutex before blocking on interrupt and
ISO transfers so other URBs on the same interface aren't starved.
- URB read timeout now skips when in-flight URBs exist (e.g. pending
interrupt transfers), preventing false idle disconnects.
- Use appropriate timeouts per transfer type: interrupt=5min,
isochronous=5s, control/bulk=1s.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add set_alt_setting() to UsbInterfaceHandler trait so host passthrough
handlers can update the physical USB interface's alternate setting via
nusb::Interface::set_alt_setting() instead of raw control transfers.
This allows nusb to find ISO endpoints after alt setting changes.
- Fix ISO OUT actual_length to equal the sum of per-packet actual_lengths
instead of transfer_buffer_length. The kernel validates this invariant
and disconnects the device on mismatch ("total length of iso packets
not equal to actual length of buffer").
Enables successful USB Audio Class 2 isochronous playback through
host passthrough.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Forward Configuration, BOS, and DeviceQualifier descriptors to the
real device in passthrough mode, preserving IADs, class-specific
descriptors (audio, HID, DFU), and endpoint companions
- Forward SET_INTERFACE to the physical device so alt settings are
actually applied (critical for audio streaming)
- Round up interrupt IN buffers to max_packet_size (matching bulk IN)
- Map USB errors to proper Linux URB status codes: STALL→EPIPE(-32),
cancelled→ENOENT(-2), timeout→ETIMEDOUT(-110)
- Propagate UrbResponse.status instead of hardcoding 0
Fixes USB Audio Class 2 device detection (kernel error "Audio class
v2/v3 interfaces need an interface association") and device reset
loops caused by EPROTO being returned for benign STALL/cancel errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes for mass storage passthrough:
1. Bulk IN buffer allocation now rounds up to the endpoint's
max_packet_size. nusb (and the USB spec) require IN transfers to be
multiples of max_packet_size. Without this, SCSI INQUIRY (36 bytes on
a 512-byte max_packet_size endpoint) was rejected, causing the kernel
to repeatedly reset the device.
2. String descriptor requests for indices not in the local string pool
are now forwarded to the device handler. This fixes interface and
configuration string descriptors (e.g., iInterface=5) that exist on
the real device but weren't populated in the synthetic string pool.
Tested: USB mass storage gadget via dummy_hcd successfully enumerates,
mounts, reads, and writes through the USBIP passthrough.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>