DF-5436 / verdict.json
{ "finding_id": "DF-5436", "status": "reproduced", "reproduced": true, "impact": "dos", "confidence": "certain", "verdict": "Mechanism-level reproduction with production code: ulpt_attach() creates two independent cdev pairs for one softc (/dev/ulptN + /dev/unlptN, ulpt.c:625-638) and the only cross-cdev exclusivity is the lock-free sc_fflags gate in unlpt_open() (ulpt.c:427) with its delayed commit (ulpt.c:456, after a ~64KB usb_alloc_mbufs kmalloc measured 27us average) and ulpt_close()s unlocked clear (ulpt.c:465). The USB framework serializes opens per fifo/vnode (usb_fifo_open curr_cpd, usb_dev.c:699-710; devfs per-vnode lock; production usb_ops is D_MPSAFE, usb_dev.c:133), not per softc, so two pinned processes free-running open/close on the two cdevs (O_RDONLY, the unprivileged direction on the 0644 nodes, ulpt.c:628) win the gate race ~50,000 times per 10 s: opens_while_open grew 53566->103639->158208 across three runs (each a successful open of an already-open direction), resets_while_open reached 311727 (ulpt_opens SOFT_RESET decision taken during an open session, ulpt.c:413), and a canary re-read inside the window (window_crossed=3) directly witnessed peer commits inside our own window. Downstream: the losing sessions read() sleeps forever (sc_fifo_open[RX] names only the last opener, ulpt.c:441; usb_read waits on cv_io, usb_dev.c:1457-1481), ulpt_reset() fires into active sessions, and the ulpt.c:730 watchdog-poll guard is defeated. With the fix.diff mechanism (sc_lock across gate..commit + sc_fifo_open NULL-clear, switched on via hw.hx5436.fixed_gate=1), three further 10 s runs added ZERO new opens_while_open and resets_while_open (158208/311727 unchanged) while EBUSY rose ~13x (the gate rejecting instead of losing). No physical/QEMU usb-printer device exists on this guest (QEMU 7.2 -device help checked) hence the KLD-harness-with-production-code evidence class; the raced logic is verbatim ulpt.c text with usb calls stubbed at equivalent cost.", "exploit_chain": "unprivileged local user (any uid; /dev/ulptN is 0644 root:operator) opens /dev/ulpt0 O_RDONLY while a second process opens /dev/unlpt0 O_RDONLY -> both pass the racy sc_fflags gate -> exclusive open broken; losers read() blocks forever (dead session DoS); either sides close clears the shared bit while the other is open -> sc_fflags desync -> subsequent same-direction opens succeed into the live session, ulpt_reset() SOFT_RESET requests fire during open, and the 1 Hz watchdog polls status during an open session (the printer-firmware-bug condition the guard exists to avoid). Repeatable at will; ~5k wins/s under contention. No memory corruption on the production path (stale sc_fifo_open[] redirection is gated safe by the USB stacks sticky CANCELLED error and sc_lock discipline; FreeBSD HEAD carries the identical staleness).", "evidence": "run.log: [R] kernel: opens_while_open=53566 ... resets_while_open=103347 (baseline run1), growth to 158208/311727 by run3, then UNCHANGED at 158208/311727 across all three fixed_gate=1 runs with EBUSY rising 6079->240055 (gate working). VERDICT.md: window_crossed=3 canary proof and 27us window measurement. panic-early-harness.txt: early-harness double-free panic proving the race corrupts shared driver state within seconds (harness infidelity documented, not a production claim).", "kernel_refs": [ "sys/bus/u4b/serial/ulpt.c:411", "sys/bus/u4b/serial/ulpt.c:427", "sys/bus/u4b/serial/ulpt.c:435", "sys/bus/u4b/serial/ulpt.c:441", "sys/bus/u4b/serial/ulpt.c:454", "sys/bus/u4b/serial/ulpt.c:456", "sys/bus/u4b/serial/ulpt.c:465", "sys/bus/u4b/serial/ulpt.c:625", "sys/bus/u4b/serial/ulpt.c:628", "sys/bus/u4b/serial/ulpt.c:730", "sys/bus/u4b/usb_dev.c:133", "sys/bus/u4b/usb_dev.c:699", "sys/bus/u4b/usb_dev.c:1457" ], "poc_changes": "Three iterations were required. (1) Initial threaded-barrier racer never raced: the two threads timeshared one CPU; rewritten as two usched_set-pinned free-running processes. (2) Harness dev_ops lacked D_MPSAFE, so dev_dopen took the kernel BGL and globally serialized all opens (zero anomalies despite 23k EBUSY overlaps); production usb_ops is D_MPSAFE (usb_dev.c:134) -- adding D_MPSAFE to the harness made the race fire immediately (and a first version whose close copy freed via sc_fifo_open[] double-freed and panicked the guest -- an infidelity vs production ulpt_close, fixed to free the closing fifos own slot per ulpt.c:468; panic text retained as panic-early-harness.txt). (3) Anomaly/ground-truth counters switched to atomic ops after counter drift blocked module unload.", "attempts": 6, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 480, "guest_dirty": 0, "build_cmd": "sh build.sh (in-tree kmod: /usr/src/sys/bus/u4b/serial/hx5436 + cc -O2 -pthread racer)", "run_cmd": "sh run.sh (kldload harness; 3x10s production-gate runs; sysctl hw.hx5436.fixed_gate=1; 3x10s fixed-gate runs; kldunload)", "code_hash": "b11a1d7d25ee839261b4b31f2657ac213db446b95cbfab2630c9517b9e366ca8", "notes": "Guest panicked once during harness development (early close-copy infidelity); vm.sh reset with-src performed, final recorded runs on a clean boot. double_open_simul is supersaturated under free-running (both cdevs mid-call almost always) and is deliberately not used as an oracle. polls_while_open stayed 0: the 1 Hz watchdog callout vs microsecond desync windows; the same desync is proven by resets_while_open which evaluates the identical condition at open-rate.", "recommended_fix": "Hold sc_lock across the sc_fflags gate..commit in unlpt_open() and the bookkeeping in ulpt_close(), and clear sc_fifo_open[dir]=NULL on close (fix.diff).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 1, "fix_verdict": "fix.diff mechanism enabled via hw.hx5436.fixed_gate=1 (sc_lock across gate..commit in open/close + sc_fifo_open NULL-clear): across three further 10 s contention runs opens_while_open and resets_while_open did not increase at all (158208/311727 before and after), while correct EBUSY rejections rose ~13x. Baseline (production gate) reproduces ~50k violations per 10 s.", "fix_evidence": "run.log lines [R] kernel: opens_while_open=158208 ... resets_while_open=311727 ... identical across the three fixed-gate runs; kernel counters after fixed runs (must equal the above) block." } |