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)
PoC verification
Evidence pack
findings/poc/DF-5437 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedBaseline (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.
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).
No comments yet.