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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5626 Β· 11 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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-GENERIC PERIPH_1284 option; the ppi entry to the peripheral path is additionally dead per DF-4806.
  • The exported API accepts any int max from KLD consumers (third-party ppbus controllers), including INT_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_testable
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5626

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).