β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5584

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5584 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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, unique bAlternateSetting/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) has do_init == 1 for BOTH the true alt-0 descriptor AND the wrapped 257th; last match wins iface->idesc = id (usb_device.c:782), so the interface's canonical descriptor β€” the one every driver probes against via usbd_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_MAX check at usb_parse.c:156; allocation ifaces_max from the same walk (usb_device.c:831-841).
  • endpoints: temp >= USB_MAX_EP_UNITS break at usb_device.c:802; allocation ep_max from the same deterministic walk (usb_device.c:844-856). ALLOC and INIT perform byte-identical walks of the same udev->cdesc under enum_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), and cdesc->wTotalLength is 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_testable

fix.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.
↓ fix.diffper-fix-DF-5584

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 before usbdi_util.h (usb_parse.c's own order); (2) add + SRCS device_if.h bus_if.h for device_t; (3) SI_SUB_DRIVERS not SI_SUB_DRIVER; (4) opt_usb.h copied from /usr/obj/usr/src/sys/X86_64_GENERIC (module include quirk); (5) build in-place without obj dir so -I. finds opt_usb.h; (6) feed all guest scripts to /bin/sh via stdin (root login shell is csh); (7) attempt-1 panic was my harness passing NULL plen to build_blob -> *plen NULL write; fixed, guest reset with-src, re-run clean.

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.