ppb_poll_bus() sleep arithmetic hz/100 rounds to 0 when the boot tunable kern.hz < 100: tsleep receives timo=0 and arms NO timeout β permanent unkillable park for the documented PPB_NOINTR sleeping poll (proven against production code at hz=50)
Summary
hz/100 is passed as tsleep timo without a floor: with the documented kern.hz<100 tunable the quotient is 0 and DF tsleep arms no callout at all. Two chained production-code steps: tsleep(...,0) slept indefinitely past the alleged 10ms; with hz transiently forced to 50 the production sleeping poll parked with its 30ms timeout never firing. In-tree only the PCATCH caller reaches the loop (signals rescue); a PPB_NOINTR non-POLL caller - a documented EXPORT_SYMS combination - parks forever unkillable. imax(1, hz/100) fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5628 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 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 | β | 129 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 | β | 737 B | view raw |
DF-5628 β ppb_poll_bus() sleep arithmetic hz/100: with the boot tunable kern.hz < 100 the poll's tsleep gets timo=0 and arms NO timeout (permanent unkillable park)
File: sys/bus/ppbus/ppb_base.c:77,83; tick source sys/kern/subr_param.c:54,199
(HZ_DEFAULT 100, TUNABLE_INT_FETCH("kern.hz", &hz)); sink
sys/kern/kern_synch.c:691 (if (timo) β timo==0 arms no callout).
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
Expected (decisive lines, see run.log)
PPBHX: DF-5628 hz=100 -> hz/100=1 tick(s) per poll sleep; ... PPBHX: DF-5628 after 6 ticks: park thread wchan==ident:1 wmesg=ppbhz0 -> STILL ASLEEP: ... PPBHX: DF-5628b hz forced to 50 ...: production ppb_poll_bus(max=3, PPB_NOINTR) on park thread ... PPBHX: DF-5628b after ~300ms ...: hz-park thread wchan==bus:1 wmesg=ppbpoll -> STILL PARKED IN PRODUCTION ppb_poll_bus: the 30ms timeout never fired (50/100==0 -> timo==0 -> no callout) PPBHX: DF-5628b hz=50 production ppb_poll_bus returned 0 only after wakeup+match ...
Two chained demonstrations against production code:
- tsleep semantics β a thread on
tsleep(..., "ppbhz0", 0)is still asleep (wchan==ident,wmesg=ppbhz0) many ticks later and wakes only on the explicitwakeup();kern_synch.c:691arms a timeout onlyif (timo). - production park at hz=50 β the harness briefly sets
hz=50(the documentedkern.hztunable value space) and runs the productionppb_poll_bus(max=3, PPB_NOINTR)on a worker thread against a never-matching peer: after the busy prologue the thread sits parked onwchan==bus, wmesg=ppbpollwith no timeout ever firing (a working 3x10ms timeout would have returned EWOULDBLOCK within ~60ms), and returns only when the harness makes the condition match and issues an explicitwakeup(bus).
In-tree callers: the only sleeping-poll caller without PPB_POLL is
byte_peripheral_write() (ppb_1284.c:410, PPB_INTR + PCATCH) β signals
still rescue it, but its 10 ms poll cadence silently degrades to
"wake-only-on-signal-or-success"; a PPB_NOINTR + non-POLL caller (none
in-tree today; the flag combination is the documented API,
ppbconf.h:123-126) parks forever, unkillable. Info severity (requires
the unusual-but-supported kern.hz<100 boot configuration).
Harness note
One shared KLD harness demonstrates DF-5625/5626/5627/5628; this pack's
lines are the DF-5628/DF-5628b ones. hz is restored to 100 at the
end of the test.
DF-5628 VERDICT β reproduced (Info, impact: dos under kern.hz<100 boot config)
Root cause
sys/bus/ppbus/ppb_base.c:77 and :83 compute the poll sleep as
hz/100 (integer division, intended "10 ms"):
tsleep((caddr_t)bus, 0, "ppbpoll", hz/100); /* :77 */
...
if (((error = tsleep((caddr_t)bus, PCATCH,
"ppbpoll", hz/100)) != EWOULDBLOCK) != 0) { /* :83 */
hz defaults to 100 (subr_param.c:54 HZ_DEFAULT 100) but is a
documented boot tunable (subr_param.c:199
TUNABLE_INT_FETCH("kern.hz", &hz); VMs commonly use lower rates). For
any kern.hz < 100 the quotient is 0, and in DragonFly tsleep()
with timo == 0 arms no timeout at all (kern_synch.c:691: the
callout is only set up if (timo)) β the sleep lasts until an explicit
wakeup(), i.e. potentially forever.
Verification
Shared ppbbasehx KLD, guest's unmodified
/usr/src/sys/bus/ppbus/ppb_base.c:
-
Sink semantics (
DF-5628lines): a worker thread ontsleep(ident, 0, "ppbhz0", 0)β still asleep (wchan==ident:1,wmesg=ppbhz0) after 6 ticks, orders of magnitude past the alleged 10 ms timeout; woke only on the explicit wakeup. Live confirmation ofkern_synch.c:691. -
Production park at hz=50 (
DF-5628blines): the harness briefly setshz = 50(restored afterwards) and runs the productionppb_poll_bus(hx_bus, 3, SELECT, 0, PPB_NOINTR)β the documented sleeping-poll flag combination β on a worker thread against a never-matching peer. After the busy prologue the thread sits parked onwchan==bus:1, wmesg=ppbpollat ~300 ms; a functioning (max-1)x10 ms timeout chain would have returned EWOULDBLOCK after ~60 ms. It returned only when the harness flipped the peer status to match and issuedwakeup((caddr_t)bus)(which also incidentally exercises thetsleep-retval-0 path β see negative notes in the audit result). Two harness iterations were needed: the first sampled at ~100 ms, while the busy prologue (measured ~200 ms in this guest) was still running, so the thread had not yet parked β the corrected 30-tick sample catches the park decisively.
Stability: identical on the decisive and stability runs (run.log,
run2.log); hz verified restored to 100 (DF-5628b hz restored to 100),
module loads/unloads clean, guest healthy afterwards.
Impact ceiling
- Requires booting with
kern.hz < 100(supported tunable; unusual config) β hence Info. - In-tree callers reaching the sleeping loop: only
byte_peripheral_write()(ppb_1284.c:410,PPB_FOREVER+PPB_INTR/PCATCH): signals still break it, but the 10 ms poll cadence collapses to wake-on-signal/condition only (a stuck-with-no-signal peer + masked signals = permanent park even for the PCATCH caller). - A
PPB_NOINTR+ non-PPB_POLL caller (none in-tree today; the exported API's documented flag combination, ppbconf.h:123-126, Makefile:13 EXPORT_SYMS) parks forever, unkillable β demonstrated against production code at hz=50.
Fix
fix.diff: hz/100 -> imax(1, hz/100) at both call sites (guarantees
at least a 1-tick timeout). Not guest-validated: proving the patched
behavior at hz<100 requires a kern.hz=50 boot (the harness's live
hz poke demonstrates the sink, not a persistent configuration) and a
single-fix kernel rebuild is disproportionate for an Info finding; the
arithmetic and semantics of both halves were verified directly
(production sleep path exercised at hz=100; park-at-timo-0 exercised
through production code at hz=50). fix_status: not_testable.
Environment
DragonFly 6.5-DEVELOPMENT x86_64 (X86_64_GENERIC, hz=100), cc 8.3, 6 vCPU QEMU guest, 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 replaces hz/100 with imax(1, hz/100) at ppb_base.c:77/:83. Not guest-validated: a faithful validation requires booting the guest with kern.hz=50 (persistent configuration, not the harness's transient poke) plus a single-fix kernel rebuild β disproportionate for an Info finding; every causal link (hz/100 arithmetic, timo==0 arms no timeout, production ppb_poll_bus parks) was separately demonstrated against production code.
fix.diff; VERDICT.md 'Fix' section; run.log DF-5628/5628b lines
Confirmed kernel references
Detail
Exploit chain
boot with kern.hz<100; any caller of the exported ppb_poll_bus(..., PPB_NOINTR) (no PPB_POLL) against an absent peer parks in 'ppbpoll' forever, unkillable (no timeout armed, no PCATCH); in-tree today only the PCATCH variant (ppb_1284.c:410) reaches the sleeping loop, which degrades to wake-on-signal-only.
Evidence (decisive lines)
run.log lines 'DF-5628 hz=100 -> hz/100=1', 'DF-5628 after 6 ticks ... STILL ASLEEP', 'DF-5628b ... wchan==bus:1 wmesg=ppbpoll -> STILL PARKED IN PRODUCTION ppb_poll_bus', 'DF-5628b hz restored to 100'; VERDICT.md
PoC changes
No seed PoC; harness written fresh. Two iterations on the hz=50 park test: first revision sampled at ~100ms which landed inside the (measured ~200ms) busy prologue and let the thread return without parking; corrected to a 30-tick (~300ms) sample β second revision catches the production park (wchan==bus, wmesg=ppbpoll).
Verified recommended fix
Use imax(1, hz/100) at both tsleep call sites so a kern.hz<100 boot can never produce a zero-timo (timeout-less) poll sleep.
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. ppb_base.c:77/:83 compute the poll sleep as integer hz/100; hz defaults to 100 (subr_param.c:54) but is the boot tunable kern.hz (subr_param.c:199), and for kern.hz<100 the quotient is 0 β while DragonFly tsleep() arms NO timeout when timo==0 (kern_synch.c:691 'if (timo)'). Demonstrated in two chained steps: (1) a thread on tsleep(...,0) stayed asleep (wchan==ident, wmesg=ppbhz0) far past the alleged 10ms and woke only on explicit wakeup; (2) with hz briefly forced to 50, the PRODUCTION ppb_poll_bus(max=3, PPB_NOINTR) parked on wchan==bus/wmesg=ppbpoll ~300ms in (busy prologue over) with its 30ms timeout never firing, returning only after the harness made the condition match and issued wakeup(bus). Confidence 'likely' rather than 'certain' only because the persistent configuration (an actual kern.hz=50 boot) was not performed; every causal link was executed against production code. Impact: with kern.hz<100 the exported sleeping-poll API degrades to timeout-less parks (unkillable for the documented PPB_NOINTR non-POLL combination, none in-tree today; the in-tree PPB_INTR caller byte_peripheral_write ppb_1284.c:410 loses its poll cadence). Info/Low: requires unusual boot config, root-gated triggers.
No comments yet.