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

uipaq_attach() stalls the per-bus USB explore thread up to ~12.8s on an unresponsive crafted device: 64 unconditional retries of the magic SET_CONTROL_LINE_STATE request (100ms timeout + ~100ms pause each) with no early-out on stall or device-removal, while holding the device enum lock

Summary

The WinCE warm-up loop retries a class request 64x with unconditional hz/10 pauses - an EP0-NAKing crafted device matching any of the ~300 VID:PID pairs forces ~12.8s (STALL/yanked still ~6.4s of pure pause) of blocking on the shared per-bus explore thread while holding the enum lock: one crafted gadget delays enumeration and detach processing for every other device on the bus, repeatable via replug. uipaq.ko auto-loads on the stock guest. Identical loop upstream (FreeBSD/OpenBSD). Hardware-gated (no QEMU device model can present a uipaq VID:PID - verified). Retry-only-on-timeout + 16-iteration fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5359 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 2.9 KB ↓ raw
fix.diff β€” 1.1 KB view raw
verdict.json β€” 4.3 KB view raw
manifest.json β€” 1.1 KB view raw

DF-5359 β€” uipaq attach-time USB-explore-thread stall (64 Γ— (100ms + 100ms))

File: sys/bus/u4b/serial/uipaq.c:1129-1136 Severity: Low (crafted-USB-device, bounded multi-second DoS of USB enumeration) PoC status: untested β€” see VERDICT.md for why this is honestly untestable on the project's QEMU rig (no QEMU USB device model can present a uipaq-table VID:PID; QEMU 7.2 exposes no vendorid/productid override for usb-net or usb-storage, and the attachable usb-serial device is 0525:7701 which binds uftdi, not uipaq).

What the bug is

uipaq_attach() sends a "magic" UCDC_SET_CONTROL_LINE_STATE control request up to 64 times before setting up transfers and attaching the ucom:

for (i = 0; i != 64; i++) {
        error = usbd_do_request_flags(uaa->device, NULL, &req,
            NULL, 0, NULL, 100);            /* 100 ms request timeout */
        if (error == 0)
                break;
        usb_pause_mtx(NULL, hz / 10);       /* ~100 ms pause, unconditional */
}
  • Device present but NAK-ing the request (crafted gadget, or wedged HW): 64 Γ— (100 ms timeout + ~100 ms pause) β‰ˆ 12.8 s.
  • Device that STALLs the request or is already gone (yanked mid-attach): requests fail fast but the pause still runs 64 Γ— ~100 ms β‰ˆ 6.4–7 s.

This loop runs inside usb_probe_and_attach() (sys/bus/u4b/usb_device.c:1286, called from uhub_explore_sub()), i.e. on the kernel USB explore thread that serially handles all hub events for the bus, while holding the device's enum lock (usbd_enum_lock, usb_device.c:1324). One crafted device therefore delays enumeration/detach of every other device on that bus for up to ~13 s per insertion, repeatably (replug).

How a human could reproduce it (needs real HW, not QEMU)

  1. Take a scriptable USB device (e.g. Facedancer21/USBProxy, or a gadget-mode dev board) and present VID:PID from uipaq_devs[] β€” e.g. 0x049f:0x0032 (Compaq iPAQ USB Sync, uipaq.c:300).
  2. Configure its EP0 to NAK (or STALL) the SET_CONTROL_LINE_STATE class-interface request (bmRequestType 0x21, bRequest 0x22).
  3. Plug it into a DragonFly host (uipaq.ko auto-loads β€” the module is built and installed: sys/bus/u4b/serial/uipaq/Makefile; /boot/kernel/uipaq.ko exists on the audit guest).
  4. Expected: ~6–13 s gap on the serial console between the uhub attach line and uipaq0 attach/transfer-setup messages; hotplug events for other devices on the same bus are delayed by the same amount.

Fix

See fix.diff β€” retry only while the request merely times out (the real "WinCE device warming up" case), give up immediately on stall/gone, and cap the retry count so the worst case is ~3 s instead of ~13 s.

Upstream note: FreeBSD sys/dev/usb/serial/uipaq.c carries the identical loop (inherited from OpenBSD/NetBSD) β€” coordinate the fix upstream.

VERDICT.md
↓ download raw

DF-5359 VERDICT β€” untested (rig cannot present the precondition; Low severity per-contract skip)

Bottom line

The defect is code-certain by arithmetic: uipaq_attach() (uipaq.c:1129-1136) retries a control request 64 times with a 100 ms request timeout and an unconditional ~100 ms inter-iteration pause, with no early-out for "device rejected" or "device gone". The loop executes on the per-bus USB explore thread inside usb_probe_and_attach() (usb_device.c:1286-1345, via uhub_explore_sub()), while the device enum lock is held (usb_device.c:1324), so a crafted device matching any of the ~300 VID:PID pairs in uipaq_devs[] stalls hub-event processing for that bus for ~6.4–12.8 s per insertion, repeatably via replug. Bounded (no unbounded loop, no memory-safety impact), hence Low.

Why untested, honestly

  • The project QEMU rig (DF-5320's) attaches a -device usb-serial = Netchip 0525:7701, which binds uftdi, not uipaq (no 0x0525 entries in uipaq_devs[], uipaq.c:150-1061).
  • QEMU 7.2 on the host exposes no vendorid/productid override for any attachable USB device model (-device usb-net,help and -device usb-storage,help were queried: no vendorid property), so a uipaq-matching VID:PID cannot be presented at all.
  • No USB host-passthrough hardware (real iPAQ / Facedacer) is available.
  • Severity is Low β†’ per the audit contract, Low/Info findings are skipped in Phase V; no guest time was spent and the guest was left untouched (status untested, attempts 0, guest_dirty 0).

What was runtime-verified (attack-surface existence only, guest was up)

  • /boot/kernel/uipaq.ko exists on the audit guest; kldstat shows it is not loaded until a matching device appears (module auto-load path via uhub/devd), i.e. the attack surface is real on a stock system the moment a crafted device is plugged in.

Fix validation

Not performed (nothing to compare against without the precondition device). fix.diff is authored from the line-level understanding: retry only while the error is USB_ERR_TIMEOUT (the genuine "device not ready yet" case), break immediately on success/stall/gone, and cap iterations at 16 so the worst case drops from ~12.8 s to ~3.2 s. fix_status = not_tested.

Kernel references

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
baseline no→ patch + rebuild →patched clean

Fix validation not applicable: no baseline reproduction was possible on the rig (no QEMU device model can present a uipaq-table VID:PID), so there is no observable to compare against. fix.diff is authored from the verified line-level root cause; the retry-only-on-timeout semantics preserve the WinCE warm-up behavior while bounding the stall.

fix.diff (git-apply-able, never applied to sys/)
↓ fix.diffper-fix-DF-5359

Confirmed kernel references

Detail

Exploit chain

Attacker with physical USB access plugs a scripted device (e.g. Facedancer/gadget board) presenting a uipaq_devs[] VID:PID (e.g. 0x049f:0x0032, uipaq.c:300) whose EP0 NAKs or STALLs the class-interface request; uipaq.ko auto-loads and uipaq_attach burns 6.4-12.8s on the bus's USB explore thread (replug to repeat), delaying enumeration/detach of all other USB devices on that bus. No escalation path - availability only.

Evidence (decisive lines)

README.md (bug arithmetic + manual Facedancer reproduction steps); VERDICT.md (why untested: no QEMU device model can present a uipaq VID:PID; usb-serial binds uftdi); fix.diff (retry-only-on-timeout + 16-iteration cap). Guest-side surface check: /boot/kernel/uipaq.ko exists, not loaded by default.

PoC changes

None - no seed PoC existed and none was buildable: the finding requires a physical/scriptable crafted USB device that NAKs or STALLs EP0 control requests, which no QEMU 7.2 device model can emulate (verified -device usb-net,help / usb-storage,help have no vendorid override; usb-serial's fixed 0525:7701 binds uftdi).

Verified recommended fix

Retry the magic request only while it times out (error == USB_ERR_TIMEOUT), break immediately on success/stall/device-gone, and cap iterations at 16 (~3s worst case instead of ~13s) - see fix.diff.

Verdict

Code-certain bounded DoS, untested on the rig because the precondition (a USB device matching uipaq's VID:PID table with an unresponsive control endpoint) cannot be presented by any QEMU 7.2 device model: -device usb-serial is Netchip 0525:7701 which binds uftdi, and no vendorid/productid override property exists for usb-net/usb-storage (verified via '-device ,help'). uipaq_attach() (uipaq.c:1129-1136) retries the magic SET_CONTROL_LINE_STATE request 64 times with a 100ms request timeout plus an unconditional ~100ms pause and no early-out on stall/device-gone; the loop runs on the per-bus USB explore thread inside usb_probe_and_attach() (usb_device.c:1286-1345) while the device enum lock is held (usb_device.c:1324), so a crafted device from the ~300-entry uipaq_devs[] table stalls all hub-event processing for that bus ~6.4-12.8s per insertion, repeatably via replug. Attack surface is real on the stock guest (/boot/kernel/uipaq.ko present, auto-loads on device match; not loaded otherwise per kldstat). Severity Low (physical crafted device, bounded delay, no memory-safety impact) -> Phase V skipped per audit contract for Low/Info findings.