usb: a configuration block is as long as the device says it is

The driver read the first 512 bytes of a configuration block into a fixed
buffer and parsed those. The block's length is the device's own choice
(wTotalLength, a u16), so anything larger was silently cut: interfaces past
the cut did not exist as far as the host was concerned, while the
SET_CONFIGURATION that follows still configured the device for all of them.
A headset is 500-900 bytes, a UVC webcam 1-3 KB, a multifunction printer
600+.

Now allocated at the declared length, so the ceiling is the field's u16 —
the specification's number rather than one of ours. A block shorter than its
own 9-byte header is refused rather than trusted.

The bring-up line reports the declared length and the bytes actually read,
so a truncation can never again be invisible, and usb-large-descriptor
asserts they match with a backreference.

That case has an honest limit, recorded in its comment: QEMU cannot produce
a block over 512 bytes. The boot keyboard, mouse and stick are 34-44, and
the largest device available is usb-audio in multi-channel mode at 211 —
which is exactly why the suite never caught this, and why it cannot now
reproduce the original trigger. What it does catch is the class: any clamp
below the attached device's block fails it, verified by pinning the buffer
to 128 and watching "config block 211 bytes, read 128" turn the case red.

Suite 115 -> 116.
This commit is contained in:
Daniel Samson
2026-08-08 11:53:01 +01:00
parent 69fbef40c0
commit 6328823ef1
4 changed files with 42 additions and 8 deletions
@@ -1311,12 +1311,25 @@ pub const Controller = struct {
const configuration = std.mem.bytesToValue(usb_abi.ConfigurationDescriptor, &header);
device.configuration_value = @intFromEnum(configuration.configuration_value);
// Read the whole block into a local buffer and parse it here (in the bus
// driver) so the parse never has to cross the 256-byte IPC boundary.
var blob: [512]u8 = undefined;
const length = @min(configuration.total_length, blob.len);
if (!self.controlTransfer(device, usb_abi.getDescriptor(.configuration, 0, 0, @intCast(length)), blob[0..length], true)) return false;
parseConfiguration(device, blob[0..length]);
// Read the whole block and parse it here (in the bus driver) so the parse
// never has to cross the 256-byte IPC boundary. Sized by the device's own
// wTotalLength — the ceiling is then the field's u16, which is the USB
// specification's, not ours.
//
// This was a fixed 512 with an `@min` clamp, which silently truncated: a
// composite device routinely exceeds it (a headset is 500-900 bytes, a UVC
// webcam 1-3 KB, a multifunction printer 600+), and the interfaces past the
// cut simply did not exist — while SET_CONFIGURATION below still configured
// the device for all of them. QEMU's boot keyboard, mouse and stick are all
// under 100 bytes, which is why it survived.
if (configuration.total_length < header.len) return false; // shorter than its own header
const blob = memory.allocator().alloc(u8, configuration.total_length) catch return false;
defer memory.allocator().free(blob);
if (!self.controlTransfer(device, usb_abi.getDescriptor(.configuration, 0, 0, configuration.total_length), blob, true)) return false;
// Both numbers, so a truncation can never again be invisible: the block the
// device declared, and the bytes actually fetched and parsed. They must match.
std.log.info("config block {d} bytes, read {d}", .{ configuration.total_length, blob.len });
parseConfiguration(device, blob);
// Select the configuration, moving the device to the configured state.
if (!self.controlTransfer(device, usb_abi.setConfiguration(configuration.configuration_value), &.{}, false)) return false;