hw/usb/u2f: unvalidated pending_in ring indices restored from migration stream -> out-of-bounds packet pointer on first IN token

hw/usb/u2f: unvalidated pending_in ring indices restored from migration stream -> out-of-bounds packet pointer on first IN token

Summary

vmstate_u2f_key (hw/usb/u2f.c:304-320) restores the pending_in ring counters as bare u8s with no domain validation: pending_in_start, pending_in_end, pending_in_num (:313-315; the array is a fixed pending_in[32][64]). The only runtime guards never constrain a restored value: u2f_pending_in_get (:225-237) checks num != 0 only, then

index = key->pending_in_start;                   /* attacker value 0..255 */
key->pending_in_start = (index + 1) % U2FHID_PENDING_IN_NUM;
--key->pending_in_num;
return key->pending_in[index];                   /* OOB: index >= 32 */

returns a pointer up to ~14 KB past the ring inside the heap-allocated device state; the caller (u2f_key_handle_data, IN token on ep1) feeds it straight to usb_packet_copy(p, packet_in, U2FHID_PACKET_SIZE) - a 64-byte host heap out-of-bounds READ on the first IN token after load (the write side u2f_pending_in_add is symmetric for pending_in_end).

Reachability notes: the only migratable entry is the u2f-passthru wrapper (hw/usb/u2f-passthru.c:508-516); u2f_passthru_post_load calls u2f_passthru_reset, which clears only the passthru-side transaction counters - it does NOT touch the U2F ring, so injected indices survive load. Reproduced on a v11.1.1 ASAN build with the fake-hidraw helper (heap-buffer-overflow via usb_packet_copy); unchanged on master 5f664cd3 (2026-09-13).

Steps to reproduce

Prerequisite: u2f-passthru realize requires a U2F hidraw device. If the host has no real U2F token, create a fully valid fake one with the attached mkfake-u2f.py (root needed only for the /dev/uhid device node; it chmods the new hidraw 666 and writes the path to /tmp/u2f-hidraw, which reproduce.sh picks up automatically):

  1. Build QEMU v11.1.1 with ASAN (x86_64-softmmu): ../configure --target-list=x86_64-softmmu --enable-asan --enable-ubsan --enable-debug --disable-werror --disable-fuzzing && ninja qemu-system-x86_64

  2. Create the fake U2F hidraw (keep the holder process running in the background): sudo nohup python3 mkfake-u2f.py >/dev/null 2>&1 &

  3. Run the reproducer (uses the hidraw recorded in /tmp/u2f-hidraw; override with HIDRAW=/dev/hidrawN if needed): QEMU=/path/to/qemu-system-x86_64 ./reproduce.sh

The qtest script is a minimal UHCI driver delivering one IN token on ep1 of the (SeaBIOS-enumerated, address 1) device after the incoming migration loaded patched.bin (from patch-stream.py over a gen-baseline.py capture; sets pending_in_start=200, end=201, num=1). The crash fires during the 64-byte copy - no further guest interaction.

Expected output (verified on a v11.1.1 ASAN build, reproducible):

ERROR: AddressSanitizer: heap-buffer-overflow
    #3 usb_packet_copy ../hw/usb/core.c:630
    #4 u2f_key_handle_data ../hw/usb/u2f.c:259

Impact

64 bytes of host heap memory adjacent to the 32-entry pending_in ring are disclosed to the guest per IN token, repeatable; the offset into adjacent memory is chosen via the migration stream (pending_in_start). Standard -device u2f-passthru,hidraw=... deployments are affected whenever the migration stream is attacker-controlled.

Suggested fix

Validate on load (post_load of the wrapper): reject pending_in_start/end >= U2FHID_PENDING_IN_NUM or pending_in_num > U2FHID_PENDING_IN_NUM, or clamp into range.

baseline.bin

gen-baseline.py

mkfake-u2f.py

patched.bin

patch-stream.py

repro.qtest

reproduce.sh