⬢ DragonFlyBSD Kernel Audit
DF-5436 / verdict.json
← back to finding ↓ download raw
{
  "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."
}