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

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)

No comments yet.

PoC verification

Evidence pack

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

  1. tsleep semantics β€” a thread on tsleep(..., "ppbhz0", 0) is still asleep (wchan==ident, wmesg=ppbhz0) many ticks later and wakes only on the explicit wakeup(); kern_synch.c:691 arms a timeout only if (timo).
  2. production park at hz=50 β€” the harness briefly sets hz=50 (the documented kern.hz tunable value space) and runs the production ppb_poll_bus(max=3, PPB_NOINTR) on a worker thread against a never-matching peer: after the busy prologue the thread sits parked on wchan==bus, wmesg=ppbpoll with 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 explicit wakeup(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.

VERDICT.md
↓ download raw

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:

  1. Sink semantics (DF-5628 lines): a worker thread on tsleep(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 of kern_synch.c:691.

  2. Production park at hz=50 (DF-5628b lines): the harness briefly sets hz = 50 (restored afterwards) and runs the production ppb_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 on wchan==bus:1, wmesg=ppbpoll at ~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 issued wakeup((caddr_t)bus) (which also incidentally exercises the tsleep-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_testable
baseline reproduced→ patch + rebuild →patched clean

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

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.