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>