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):
-
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 -
Create the fake U2F hidraw (keep the holder process running in the background):
sudo nohup python3 mkfake-u2f.py >/dev/null 2>&1 & -
Run the reproducer (uses the hidraw recorded in /tmp/u2f-hidraw; override with
HIDRAW=/dev/hidrawNif 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:259Impact
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.