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

ulpt(4): unprivileged open(/dev/ulptN) stalled ~4s uninterruptibly by an evil printer NAKing SOFT_RESET (SIGKILL-immune)

Summary

ulpt_open runs ulpt_reset whenever sc_fflags==0: two sequential sync EP0 requests each 2s-timeout, on a framework lksleep with NO PCATCH - SIGKILL cannot break it, only the timeout. An evil NAK-ing printer pins every opening thread ~4s D-state; 0644 cdev = any unpriv user with plain open(), loopable for thread exhaustion. Harness: SIGKILLed child reaped only at 4.014/4.017/4.015s; 250ms-timeout fix reaps at ~0.5s. The ucom cfg-stall family, ulpt instance.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5437 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.0 KB ↓ raw
hx5437_stall.c β€” 4.5 KB view raw
hx5437_probe.c β€” 1.6 KB view raw
Makefile β€” 63 B ↓ download
build.sh β€” 96 B view raw
run.sh β€” 98 B view raw
build.log β€” 6.9 KB view raw
run.log β€” 2.3 KB view raw
env.txt β€” 222 B view raw
fix.diff β€” 1.1 KB view raw
manifest.json β€” 828 B view raw
verdict.json β€” 4.3 KB view raw

DF-5437 β€” unprivileged open(/dev/ulptN) can be stalled ~4 s uninterruptibly by an evil printer

What (one line)

ulpt_open() runs ulpt_reset() whenever it observes sc_fflags == 0 (ulpt.c:413-418), and ulpt_reset() issues two sequential synchronous EP0 control requests with a 2 s timeout each (2 * USB_MS_HZ, ulpt.c:180 and ulpt.c:183). The wait inside usbd_do_request_flags() is

while (usbd_transfer_pending(xfer)) {
        lksleep(xfer, xfer->xroot->xfer_lock, 0, "WXFER", hz);   /* no PCATCH */
}

(usb_request.c:648-650) β€” uninterruptible: SIGKILL cannot break it; only the per-request xfer timeout ends it. A printer that NAKs the data/status stage of SOFT_RESET (trivial for an evil device β€” the USB spec allows indefinite NAKing) therefore pins every opening thread for ~4 s of unkillable sleep. /dev/ulptN is 0644 (ulpt.c:628), so any unprivileged local user triggers this with open("/dev/ulpt0", O_RDONLY); an open-loop pins nthreads. Bounded per open (2x2 s + the 1 s sleep granularity of the loop), repeatable per open call.

Evidence class

No usb-printer device model on the guest QEMU 7.2 (and the guest kernel's USB_REQ_DEBUG fault-injection sysctls are not built into X86_64_GENERIC) β†’ KLD harness with the production chain shape and production constants (DF-5383 class): hx5437_stall.c implements, verbatim, the wait loop (lksleep(..., 0, "WXFER", hz)), the two-sequential-request structure of ulpt_reset (ulpt.c:179-183), and the default timeout 2000 ms = 2 * USB_MS_HZ. The "device NAKs until timeout" behavior is modelled by a callout that completes the request exactly at the timeout β€” which is what the xfer timeout does to a NAKed control transfer in production.

Build / Run (in guest)

sh build.sh
sh run.sh

hx5437_probe forks a child that opens the harness cdev; the parent SIGKILLs it 100 ms in; waitpid measures how long the child actually survives.

Expected decisive output

[R] SIGKILL sent at +100 ms; child reaped after 4.014 s (status=0x9)
[R] VERDICT: opener survived SIGKILL for 4.0 s -- uninterruptible stall reproduced
... (x3 baseline: 4.014 / 4.017 / 4.015 s; dmesg: ELAPSED=4000 ms x3)
=== fix mechanism (timeout_ms=250) ===
[R] SIGKILL sent at +100 ms; child reaped after 0.515 s (status=0x9)
... (dmesg: ELAPSED=500/501 ms)

The child carries the pending SIGKILL through the full 4 s of kernel sleep β€” it cannot be killed, scheduled away, or interrupted β€” then dies the moment the syscall returns. With the fix mechanism (per-request timeout bounded to USB_MS_HZ/4 = 250 ms, fix.diff) the same probe bounds the stall at ~0.5 s.

Fix

fix.diff bounds both SOFT_RESET requests to USB_MS_HZ / 4 (250 ms): SOFT_RESET carries no data, 250 ms is ample for a functional printer, and worst-case open() stall drops from ~4 s to ~0.5 s. (Framework-level hardening β€” PCATCH on the WXFER sleep β€” would additionally make the stall signal-interruptible; that is usb_request.c scope, noted for upstream.)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched reproduced

Baseline (production 2x2000 ms): opener survives SIGKILL 4.014/4.017/4.015 s, dmesg ELAPSED=4000 ms x3. Fix mechanism (USB_MS_HZ/4 per request per fix.diff): 0.515/0.500 s, dmesg ELAPSED=500/501 ms -- 8x reduction, stall bounded below the uninterruptibility oracle.

run.log baseline and fix sections; dmesg 'HX5437: open() complete: ELAPSED=...' lines.
↓ fix.diffper-fix-DF-5437

Confirmed kernel references

Detail

Exploit chain

evil USB printer (printer-class descriptors, BI alt setting) enumerates normally, then NAKs the data/status stage of both SOFT_RESET request forms; any unprivileged local user's open("/dev/ulptN", O_RDONLY) (world-readable 0644 node) blocks uninterruptibly ~4 s per open; a loop of opens from N processes pins N kernel threads in D-state, immune to SIGKILL, repeatable indefinitely (device-triggered local DoS amplification). Bounded per open; no memory corruption.

Evidence (decisive lines)

run.log: '[R] SIGKILL sent at +100 ms; child reaped after 4.014 s (status=0x9)' x3 baseline, then 'reaped after 0.515 s' with the fix mechanism; dmesg tail: 'HX5437: open() complete: ELAPSED=4000 ms (req1=0 req2=0, timeout_ms=2000)' x3 and 'ELAPSED=500/501 ms ... timeout_ms=250'.

PoC changes

Harness is first-iteration correct (verbatim wait-loop shape and production constants); run.sh adjusted to tolerate the fix-probe's exit code (rc=1 when the stall drops below the 1 s oracle -- that is the fix working).

Verified recommended fix

Bound both ulpt_reset() SOFT_REQUEST timeouts to USB_MS_HZ/4 (fix.diff); optionally make the WXFER wait PCATCH-interruptible in usb_request.c.

Verdict

Mechanism-level reproduction with production chain shape and constants: ulpt_open() runs ulpt_reset() when it observes sc_fflags==0 (ulpt.c:413-418), which issues two sequential synchronous EP0 control requests each with a 2*USB_MS_HZ (2000 ms) timeout (ulpt.c:180,183); the wait loop inside usbd_do_request_flags() is lksleep(xfer, xfer_lock, 0, "WXFER", hz) with NO PCATCH (usb_request.c:648-650), uninterruptible by any signal including SIGKILL. An evil printer that NAKs the control data/status stage stretches every open() to the full 2x2 s (plus the 1 s loop granularity). The harness implements the verbatim wait loop and the two-sequential-request structure with the production timeout; the child opener SIGKILLed at +100 ms was reaped only at 4.014/4.017/4.015 s across three runs (status=0x9: the pending SIGKILL is carried through the whole kernel sleep and lands only at syscall exit), dmesg showing ELAPSED=4000 ms x3. /dev/ulptN is 0644 (ulpt.c:628) so the trigger is a plain unprivileged open(); an open-loop pins nthreads each for ~4 s unkillable. With the fix.diff mechanism (per-request timeout bounded to USB_MS_HZ/4 = 250 ms, mirrored via hw.hx5437.timeout_ms=250) the same probe bounds the stall at 0.500-0.515 s (dmesg ELAPSED=500/501 ms). No usb-printer device model exists in guest QEMU 7.2 and X86_64_GENERIC lacks the USB_REQ_DEBUG fault-injection sysctls, hence the KLD-harness evidence class (DF-5383 precedent).