ppb_poll_bus() PPB_POLL path busy-spins max*10000 DELAY(1) iterations with no sleep, no signal checkpoint and no clamp on max β unkillable CPU-livelock windows (measured ~20s for the in-tree max=100 call)
Summary
PPB_NOINTR|PPB_POLL is numerically just PPB_POLL, so the whole wait runs as max x 10000 busy port-read+DELAY(1) iterations: measured exactly 10000*max register reads and 19.67s CPU pin for the in-tree max=100 (do_peripheral_wait's argument, nominal 1s) - a sleeping poll does ~10000x fewer. Reachable on GENERIC via 0600 /dev/lpt0 negotiation reads (40ms windows); the exported API accepts arbitrary KLD max (INT_MAX ~ 8 months). SIGKILL cannot shorten a window. DF-5565 livelock family; yield-a-tick-between-windows fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5626 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.1 KB | β raw | |
| VERDICT.md | β | 4.0 KB | β raw | |
| harness.c | β | 15.2 KB | view raw | |
| Makefile | β | 273 B | β download | |
| build.sh | β | 143 B | view raw | |
| run.sh | β | 149 B | view raw | |
| build.log | β | 7.9 KB | view raw | |
| run.log | β | 2.8 KB | view raw | |
| run2.log | β | 5.4 KB | view raw | |
| env.txt | β | 411 B | view raw | |
| fix.diff | β | 627 B | view raw |
DF-5626 β ppb_poll_bus() PPB_POLL path: uninterruptible busy-spin scaled by unclamped max (CPU livelock primitive)
File: sys/bus/ppbus/ppb_base.c:60-67; in-tree argument suppliers
sys/bus/ppbus/ppb_1284.c:58 (max=4), :64 (max=100), :704 (max=40,
PERIPH_1284-gated).
Build (inside the DragonFly guest)
sh build.sh # compiles VERBATIM /usr/src/sys/bus/ppbus/ppb_base.c into ppbbasehx.ko
Run (inside the guest)
sh run.sh # NOTE: the max=100 test pins one CPU for ~20 s in this guest
Expected (decisive lines, see run.log)
PPBHX: DF-5626 busy max=5 : ret=35 rstr_reads=50000 wall=1000010us PPBHX: DF-5626 busy max=100: ret=35 rstr_reads=1000000 wall=19670196us PPBHX: DF-5626 sleep ctrl max=3: ret=35 rstr_reads=10002 wall=240003us
When how & PPB_POLL, the outer loop runs max times and each iteration
executes 10000 x (status-register read + DELAY(1)): a pure busy loop with
no tsleep, no signal checkpoint, and no clamp on max. Proven by the
port-read counter: exactly 10000 register reads per max unit (a
sleeping poll reads the status once per 10 ms window β the control line
shows 10002 reads for the identical wall-order wait without PPB_POLL).
Measured wall time on this 6-vCPU guest: max=5 (nominal 50 ms) pins the
CPU for ~1.0 s; max=100 β do_peripheral_wait()'s exact in-tree
argument (ppb_1284.c:64) β pins it for ~19.7 s (DELAY(1)+dispatch
overhead ~20 us/iteration under QEMU; ~1 s on bare metal).
Reachability: ppi/lpt device nodes are 0600 root:wheel (ppi.c:164-166,
lpt.c:399-402), so the trigger is root-gated; the 1 s max=100 path is
additionally behind the non-GENERIC PERIPH_1284 option (its ppiwrite
entry is further dead per DF-4806). The reachable-on-GENERIC spin is
do_1284_wait's 40 ms per negotiation (lptread/ppiread/vpo paths). Low
severity: root-triggerable CPU livelock windows; API export
(sys/bus/ppbus/Makefile:13) lets any KLD caller pass an arbitrary max
(security boundary for third-party ppbus controllers).
Harness note
One shared KLD harness demonstrates DF-5625/5626/5627/5628; this pack's
lines are the DF-5626 ones.
DF-5626 VERDICT β reproduced (Low, impact: dos / CPU livelock)
Root cause
sys/bus/ppbus/ppb_base.c:60-67:
for (j = 0; j < ((how & PPB_POLL) ? max : 1); j++) {
for (i = 0; i < 10000; i++) {
r = ppb_rstr(bus);
DELAY(1);
if ((r & mask) == status)
return (0);
}
}
With PPB_POLL set the whole wait is max x 10000 busy iterations of
DELAY(1) + a port read: no sleep, no signal checkpoint, no preemption
point, no clamp on max (a caller-controlled int, exported via
ppbus.ko's EXPORT_SYMS, Makefile:13). PPB_NOINTR | PPB_POLL β
what all in-tree poll callers pass (ppb_1284.c:58, :64, :704) β is
numerically just PPB_POLL (ppbconf.h:126,129: PPB_NOINTR is 0), so the
author's "don't use PCATCH" intent silently becomes "busy-spin the entire
window".
Verification (production code, measured)
The shared ppbbasehx KLD compiles the guest's unmodified
/usr/src/sys/bus/ppbus/ppb_base.c and calls ppb_poll_bus() against a
never-matching scripted peer (status=0xD0, poll requires SELECT==0).
The fake controller counts status-register reads:
| call | rstr reads | wall | interpretation |
|---|---|---|---|
max=5, PPB_NOINTR\|PPB_POLL |
exactly 50000 | 1.00 s | 5 x 10000 busy iterations, zero sleeps |
max=100, PPB_NOINTR\|PPB_POLL |
exactly 1000000 | 19.67 s | do_peripheral_wait's argument: ~20 s CPU pin |
max=3, PPB_NOINTR (control) |
exactly 10002 | 0.24 s | 10 ms busy prologue + 2 condition checks + 2 tsleeps |
The read-count is the decisive discriminator: a sleeping poll of the same
nominal duration performs ~10000x fewer register reads. Nominal duration
is max x 10 ms; the measured overshoot (~20x) is the cost of
DELAY(1) + newbus method dispatch per iteration in this guest β i.e.
the livelock window is worse under virtualization, exactly the
environment parallel-port-enabled VMs run in.
Stability: identical structure across 3 loads (run.log, run2.log, earlier build), wall times within 3%.
Impact ceiling
- Root-gated local CPU-DoS windows: reachable on GENERIC via
do_1284_wait(max=4 β 40 ms busy per IEEE1284 event wait; callers lptread (lpt.c:695), ppiread (ppi.c:342), vpo/immio negotiate) from 0600 root:wheel device nodes. do_peripheral_wait(max=100 β 1 s nominal, ~20 s measured here) and the negotiate PERIPH branch (max=40) are compiled into every kernel but gated behind the non-GENERICPERIPH_1284option; the ppi entry to the peripheral path is additionally dead per DF-4806.- The exported API accepts any
int maxfrom KLD consumers (third-party ppbus controllers), includingINT_MAX(β 8 months of busy-spin). - Uninterruptible during each 10 ms-to-seconds window; SIGKILL cannot shorten it, other threads on the pinned CPU starve at equal priority.
Low severity (root trigger; DF-5565 livelock family).
Fix
fix.diff: after each 10 ms busy window (when further windows remain),
tsleep(bus, PCATCH, "ppbpoll", 1) and abort on non-EWOULDBLOCK β keeps
the total poll duration, makes the wait preemptible and
signal-cancellable, kills the pin. Not fix-validated on the guest: the
behavioral delta is scheduler-observable (preemption during the window),
which a same-thread harness cannot self-measure honestly, and a
single-fix kernel rebuild is disproportionate for a Low finding;
rstr_reads/window arithmetic of the patched function re-verified by
inspection (loop body unchanged, only inter-window yield added).
fix_status: not_testable.
Environment
DragonFly 6.5-DEVELOPMENT x86_64 (X86_64_GENERIC), cc 8.3, 6 vCPU QEMU guest, hz=100, no parallel port.
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_testablefix.diff adds the inter-window yield (ppb_base.c:60-67 loop body unchanged, only a bounded tsleep(bus, PCATCH, 1) + error abort appended per outer iteration). Not guest-validated: the behavioral delta is scheduler preemption during the window, which a same-thread harness cannot honestly self-observe, and a single-fix kernel rebuild is disproportionate for this Low/DoS finding; the patched loop was re-derived by inspection (read counts unchanged, one sleep added per remaining window).
fix.diff; VERDICT.md 'Fix' section
Confirmed kernel references
Detail
Exploit chain
root (or KLD ppbus controller) issues repeated lppbus reads/negotiations against an absent/silent peripheral; each do_1284_wait burns a 40ms busy window (GENERIC), each do_peripheral_wait burns >=1s nominal (~20s measured under QEMU) with PERIPH_1284; looping pins the CPU and starves equal-priority threads for the window durations; SIGKILL cannot shorten a window.
Evidence (decisive lines)
run.log lines 'DF-5626 busy max=5 ... rstr_reads=50000 wall=1000010us', 'busy max=100 ... rstr_reads=1000000 wall=19670196us', 'sleep ctrl max=3 ... rstr_reads=10002'; run2.log (stability); VERDICT.md table
PoC changes
First harness revision used a scripted status byte of 0xC0 whose SELECT bit (0x10) is clear, so the poll matched instantly (rstr=1, wall=0); corrected to 0xD0 (SELECT set) to make the condition unreachable β second revision reproduced.
Verified recommended fix
Insert a 1-tick PCATCH tsleep between consecutive 10ms busy windows (and abort on non-EWOULDBLOCK) so long PPB_POLL waits stay preemptible and signal-cancellable.
Verdict
Reproduced via the KLD-harness-with-production-code evidence class: the unmodified /usr/src/sys/bus/ppbus/ppb_base.c compiled into ppbbasehx.ko was driven against a never-matching scripted peer. ppb_base.c:60-67: with PPB_POLL set the entire wait is max*10000 busy DELAY(1)+port-read iterations β no tsleep, no signal checkpoint, no clamp on max. Decisive metrics: rstr_reads == exactly 50000 for max=5 and exactly 1000000 for max=100 (a sleeping poll of the same nominal duration performs ~10000x fewer reads; control run without PPB_POLL shows 10002), and measured wall times of 1.00s (max=5, nominal 50ms) and 19.67s (max=100 β do_peripheral_wait's exact in-tree argument, ppb_1284.c:64, nominal 1s) of pinned CPU per call. PPB_NOINTR|PPB_POLL is numerically just PPB_POLL (ppbconf.h:126/:129), so the busy spin is what every in-tree poll caller gets. Root-gated reachability on GENERIC (do_1284_wait max=4 via lptread/ppiread, 0600 nodes); max=100 path behind non-GENERIC PERIPH_1284; exported API accepts arbitrary KLD max. Low severity CPU-livelock (DF-5565 family).
No comments yet.