usb_idesc_foreach() uint8_t alt-index wraparound on configs with >256 alternate settings (missing upstream FreeBSD 6da5df47 fix)
Summary
The alt-setting counter is uint8_t and increments on every consecutive same-bInterfaceNumber descriptor: a crafted config with 257+ interface descriptors wraps 255->0, so usb_config_parse(INIT, alt=0) matches BOTH the true alt-0 and the wrapped 257th descriptor - iface->idesc aliases onto the attacker's descriptor that every driver probes against (class/protocol/endpoint confusion), ep_curr resets mid-walk, and usbd_get_no_alts reports 255 for a true 300 desynchronizing the SET_INTERFACE validator and ugen ALT_SELECT. KLD harness on the real in-kernel symbols: WRAP_AT=257 deterministic x2 (256-alt control clean). Logic confusion only - all consumer arrays stay capped (USB_IFACE_MAX/USB_MAX_EP_UNITS), no OOB/UAF primitive. DFly's r250204 import predates upstream FreeBSD 6da5df47 (PR 251856) - the file's only functional divergence; port-verbatim fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5584 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| altwrap.c | β | 6.3 KB | view raw | |
| Makefile | β | 75 B | β download | |
| build.sh | β | 174 B | view raw | |
| run.sh | β | 155 B | view raw | |
| build.log | β | 1.2 KB | view raw | |
| run.log | β | 5.8 KB | view raw | |
| run.2.log | β | 1.2 KB | view raw | |
| env.txt | β | 361 B | view raw | |
| VERDICT.md | β | 5.5 KB | β raw | |
| fix.diff | β | 1.2 KB | view raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-5584 VERDICT β REPRODUCED (logic-level; no memory-safety impact in current consumers)
The defect
sys/bus/u4b/usb_parse.c:142-151 β usb_idesc_foreach() counts alternate
settings of an interface in ps->iface_index_alt, a uint8_t
(sys/bus/u4b/usbdi_util.h:37). When a configuration descriptor contains
more than 256 interface descriptors sharing one bInterfaceNumber
(fully attacker-chosen bytes from a malicious USB device), the
ps->iface_index_alt ++ at usb_parse.c:150 wraps 255 β 0 on the 257th
descriptor, so the 257th+ alternate settings are walked as if they were
alt 0, 1, 2, β¦ again.
DragonFly's tree (imported at FreeBSD r250204, 2013) predates the upstream
fix: FreeBSD commit 6da5df470099fc55f20b03ef364af730e7be4ffe
("Improve handling of alternate settings in the USB stack.", hselasky,
2020-12-15, PR 251856, submitted by Horse Ma / Dell) adds exactly the
if (ps->iface_index_alt == 255) continue; guard. Diff of DF's file vs
FreeBSD head shows this is the only functional divergence.
Reproduction (guest-live, real kernel symbols)
Harness: altwrap.c KLD calling the real in-kernel
usb_idesc_foreach() / usbd_get_no_alts() (symbols confirmed in
/boot/kernel/kernel: ffffffff80a508b0 T usb_idesc_foreach,
ffffffff80a50a00 T usbd_get_no_alts) on crafted blobs:
- EVIL blob: valid 9-byte UDESC_CONFIG header (wTotalLength=2709) +
300 interface descriptors, all
bInterfaceNumber=0, uniquebAlternateSetting/subclass tag per descriptor. - CTRL blob: same with exactly 256.
Observed (run.log, run.2.log β deterministic across 2 loads):
EVIL idesc#256: iface_index_alt=255 EVIL idesc#257: iface_index_alt=0 <-- 8-bit WRAP EVIL walk ended: max_alt=255, WRAP_AT=257 usbd_get_no_alts()=255 while true alt count=300 CTRL walk ended: max_alt=255, WRAP_AT=0 (boundary control clean) RESULT: WRAP_REPRODUCED
Note attempt 1 panicked the guest β my harness's own bug (passing
NULL as plen to build_blob() β *plen = total NULL write inside
the KLD, before any parser call). Fixed; guest reset; attempt 2 and 3
clean. The parser was never implicated.
Downstream effect (code-traced; replica demonstrated)
usb_config_parse() (sys/bus/u4b/usb_device.c:753-826) consumes the walk:
- usb_device.c:775
if (ips.iface_index_alt == 0) ep_curr = ep_max;β the wrapped 257th descriptor is treated as a new base setting: the interface's endpoint slot base is re-derived mid-walk, so the wrapped alt's endpoints occupy fresh slots above the interface's real base. - usb_device.c:760-770 β a full USB_CFG_INIT selecting alt 0
(
usbd_set_config_index()β usb_device.c:648) hasdo_init == 1for BOTH the true alt-0 descriptor AND the wrapped 257th; last match winsiface->idesc = id(usb_device.c:782), so the interface's canonical descriptor β the one every driver probes against viausbd_get_interface_descriptor()β becomes the attacker's 257th choice (class/subclass/protocol/bNumEndpoints confusion, mismatched against the alt the stack actually negotiates). - The replica of that matching loop over the real walk confirms:
do_init matched 2 descriptors; iface->idesc would land on the wrapped descriptor (ALIASED). usbd_get_no_alts()(usb_parse.c:267) returns 255 for a true count of 300, so the SET_INTERFACE validator usb_handle_request.c:193 and the ugen ALT_SELECT path (usb_generic.c:644) reason on a count that no longer matches the walked reality.
Why NOT memory corruption (impact ceiling)
All consumer arrays remain bounded regardless of the wrap:
- interfaces:
ps->iface_index >= USB_IFACE_MAXcheck at usb_parse.c:156; allocationifaces_maxfrom the same walk (usb_device.c:831-841). - endpoints:
temp >= USB_MAX_EP_UNITSbreak at usb_device.c:802; allocationep_maxfrom the same deterministic walk (usb_device.c:844-856). ALLOC and INIT perform byte-identical walks of the sameudev->cdescunderenum_lock(usb_device.c:643-648), so the wrap affects both passes symmetrically β indices never exceed the allocated counts. - Every walked descriptor is in-bounds by
usb_desc_foreach()'s own checks (usb_parse.c:89-99), andcdesc->wTotalLengthis forced to the allocation size at fetch (usb_request.c:1366-1376).
Conclusion: attacker-triggerable correctness/invariant break with driver-confusion potential (and a divergence from a validated upstream security fix), but no OOB write/read, UAF, or reachable panic was demonstrable from it in this tree. Escalation to uid0: not applicable (no corruption primitive).
Fix
fix.diff in this directory: byte-for-byte port of upstream FreeBSD
6da5df47. With the fix, the 257th+ same-numbered descriptor is skipped
(continue) and the walk terminates at the 256th β the CTRL-blob
behavior. Kernel-rebuild validation was not performed (per audit policy,
rebuild validation is reserved for memory-corruption findings; this is a
Low-severity logic/hardening finding and the hunk is already main-line
validated upstream). fix_status: not_testable.
Guest
DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
(X86_64_GENERIC), kernel built from this tree. Guest reset to
with-src snapshot after the run.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff is a byte-for-byte port of upstream FreeBSD 6da5df47 (PR 251856), already validated in FreeBSD main+stable; per audit policy the single-fix kernel rebuild is reserved for memory-corruption findings, and this one is a Low-severity logic/hardening defect with no corruption primitive, so the patched-kernel run was not performed. Baseline (unpatched) behavior reproduced twice as WRAP_AT=257; with the upstream hunk the walk provably terminates at the 256th descriptor (the CTRL-blob behavior).
fix.diff (ported upstream hunk); run.log baseline WRAP_AT=257; CTRL 256-alt clean walk demonstrates the post-fix termination shape.
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log / run.2.log: 'EVIL idesc#256: iface_index_alt=255' then 'EVIL idesc#257: iface_index_alt=0' and 'RESULT: WRAP_REPRODUCED (evil wrap at idesc#257; 256-alt control clean)'; 'usbd_get_no_alts()=255 while true alt count=300'; 'replica usb_config_parse(INIT, alt=0): do_init matched 2 descriptors; iface->idesc ... ALIASED onto wrapped descriptor'. build.log: clean -Werror KLD build; nm confirms ffffffff80a508b0 T usb_idesc_foreach in /boot/kernel/kernel (env.txt). VERDICT.md: full narrative incl. attempt-1 harness NULL-write panic (own bug, guest reset). fix.diff: port of upstream FreeBSD 6da5df47.
PoC changes
Seed-free (new harness). Fixes vs first draft: (1) include
Verified recommended fix
Port upstream FreeBSD 6da5df47: in usb_idesc_foreach(), skip further same-bInterfaceNumber descriptors once ps->iface_index_alt == 255 (see fix.diff).
Verdict
Reproduced guest-live against the real in-kernel usb_idesc_foreach(): a crafted configuration descriptor with 257+ interface descriptors sharing one bInterfaceNumber wraps the uint8_t ps->iface_index_alt counter 255->0 at walk position 257 (dmesg WRAP_AT=257; 256-alt control clean), because DragonFly's r250204-era import predates upstream FreeBSD commit 6da5df47 (PR 251856) which caps alt settings at 256. Downstream (code-traced + replica over the real walk): usb_config_parse's full INIT selecting alt 0 matches BOTH the true alt-0 and the wrapped 257th descriptor, so iface->idesc lands on the attacker's 257th descriptor (driver probe/endpoint confusion), the ep_curr slot base resets mid-walk, and usbd_get_no_alts() reports 255 for a true 300 so the SET_INTERFACE validator reasons on a stale count. Impact ceiling is logic confusion only: every consumer array stays bounded (USB_IFACE_MAX=32 at usb_parse.c:156, USB_MAX_EP_UNITS=32 at usb_device.c:802, ALLOC/INIT walk deterministically and identically under enum_lock), and wTotalLength is forced to the allocation size at fetch (usb_request.c:1376) - no OOB/UAF/panic primitive was demonstrable, so no uid0 chain exists for this bug. Classification: attacker-triggerable invariant break / missing validated upstream security fix; Low severity hardening, not memory corruption.
No comments yet.