pps_fetch_timeout(): normalized finite sub-tick timeout inverts into an unbounded sleep (tstohz_low()==0 -> tsleep with no timeout armed)
Summary
PPS_IOC_FETCH with a perfectly normalized finite timeout of {0, 1..nstick-1 ns} converts to a tick count of 0, and tsleep(pps, PCATCH, 'ppsfch', 0) arms NO timeout at all (kern_synch guards the callout with if (timo)), so the FETCH sleeps indefinitely (only a PPS edge or signal can break it) where the PPS API requires an immediate ETIMEDOUT. Reproduced live on the guest: FETCH with {0 s, 1 ns} on /dev/ttyd0 was still asleep in wchan 'ppsfch' at t=12 s; only kill -9 reaped it. Root-gated on default installs (0600 ttyd0) and confined to the caller's own signal-killable thread, hence Info. Also live-REFUTED the amplification theory (tty_token wedge): DragonFly LWKT tokens release when the holder blocks β concurrent GETCAP ran in 0.000s while FETCH slept. Clamp-to==1 fix dry-run validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5834 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ppslockdemo.c | β | 3.2 KB | view raw | |
| build.sh | β | 96 B | view raw | |
| run.sh | β | 132 B | view raw | |
| build.log | β | 286 B | view raw | |
| run.log | β | 1.5 KB | view raw | |
| env.txt | β | 461 B | view raw | |
| VERDICT.md | β | 4.2 KB | β raw | |
| fix.diff | β | 309 B | view raw | |
| verdict.json | β | 3.4 KB | view raw |
DF-5834 VERDICT
Status: reproduced (behavioral inversion of a finite timeout into an unbounded sleep) β impact: none (Info-grade: root-gated on the default install; hang is confined to the calling thread; PCATCH/kill -9 recovers). The originally-suspected global tty DoS amplification was tested and refuted β details below.
Root cause (path:line)
sys/kern/kern_clock.c:1672βPPS_IOC_FETCHenters the timeout path for any nonzero timeout:if (fapi->timeout.tv_sec != 0 || fapi->timeout.tv_nsec != 0).sys/kern/kern_clock.c:1616-1618βto = INT_MAX; if (timeout->tv_sec > -1) to = tstohz_low(timeout);For{tv_sec:0, tv_nsec:1}the condition is true (0 > -1), soto = tstohz_low({0,1}).sys/kern/kern_clock.c:1272βtstohz_low:ticks = (int)(sec * hz + (u_long)ts->tv_nsec / nstick)=(int)(0 + 1/10000000)= 0.sys/kern/kern_clock.c:1626βtsleep(pps, PCATCH, "ppsfch", 0).sys/kern/kern_synch.c:691βif (timo)guards the timeout callout, sotimo == 0means "no timeout": the thread sleeps untilwakeup(pps)(a PPS edge β none on an idle serial line) or a signal (PCATCH).EWOULDBLOCKnever occurs, so thetv_sec < 0 β continueretry at kern_clock.c:1628-1630 is dead for this input; the caller hangs.
PPS API semantics: {0, 1 ns} is a perfectly normalized finite timeout that
must return ETIMEDOUT (no event) essentially immediately.
How it was reproduced (guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, hz=100)
Single-process fork rig (ppslockdemo.c): one fd on /dev/ttyd0 (sio0, the
serial console; ppscap=0x1133 = CAPTUREASSERT|CAPTURECLEAR|TSFMT|CANWAIT, so
pps_ioctl is definitively reached via sio.c:2165).
- Control (
ppslockdemo 0): FETCH timeout{0,0}skips the timeout path (kern_clock.c:1672 false), returns instantly; parentPPS_IOC_GETCAPelapsed 0.000 s. β - 8 s finite wedge (
ppslockdemo 8): child issuedPPS_IOC_FETCHwith{8,0}; mid-wedgepsshows the child in state S4, wchanppsfch; child's FETCH returned -1/ETIMEDOUT at tβ8 s. β - Sub-tick finite hang (
ppslockdemo inf): child issued FETCH with{0 s, 1 ns}β a FINITE 1-nanosecond request. At t=12 s the child was still asleep in wchanppsfchwith no timeout armed; onlykill -9reaped it. This is the reproduced defect. β
Amplification theory refuted (negative result, kept as evidence)
sioioctl() holds the global tty_token (taken at sio.c:294) across the
pps_ioctl() call in its default: case (sio.c:2165-2171), so an unbounded
sleep there looked like a system-wide tty DoS. Refuted on two independent
grounds:
- Source:
lwkt_switch()releases all tokens of a blocking thread βsys/kern/lwkt_thread.c:636-643(lwkt_relalltokens(td)), the documented DragonFly token discipline (tokens protect running critical sections, they are not held across blocking). - Live: during the 8 s wedge (child asleep in
ppsfch"holding" the token by lexical scope), the parent'sPPS_IOC_GETCAPβ which must acquiretty_tokenthrough the samesioioctl()entry β completed in 0.000 s (run.log, "WEDGE 8s" section). No other tty on the system wedges.
Fix
fix.diff β after to = tstohz_low(timeout), clamp to == 0 β 1 so any
finite timeout still arms a timeout; tv_sec < 0 keeps INT_MAX (intentional
blocking fetch). Authored from line-accurate root cause; kernel rebuild +
patched retest not performed for this Info-grade finding
(fix_status: not_tested).
Attempts / iteration
3 rig runs that count (control, 8 s wedge, inf) after one rig fix: the first
inf invocation parsed atoi("inf") == 0 and silently ran the control; fixed
to explicit string-compare (see poc_changes in verdict.json). No guest
dirty state remains (all test binaries and outputs removed; guest still up).
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 authored from line-accurate root cause (one-line clamp after to=tstohz_low()); kernel rebuild + patched retest not performed for this Info-grade, non-corruption finding.
findings/poc/DF-5834/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: 'inf' section β child prints 'PPS_IOC_FETCH timeout=0s 1ns', ps at t=12s shows pid 971 S0 wchan=ppsfch with no 'FETCH returned' line until kill -9; control section β timeout {0,0} returns instantly, GETCAP elapsed 0.000s; 'WEDGE 8s' section β mid-wedge ps shows ppsfch sleeper while parent GETCAP elapsed 0.000s (token-refutation). VERDICT.md has the full path:line trace.
PoC changes
Original rig used two processes (wedge + victim with separate opens of /dev/ttyd0); redesigned into a single fork-based program (one fd, child wedges, parent measures GETCAP). First 'inf' invocation hit a rig bug (atoi("inf")==0 ran the control); fixed to explicit strcmp. No kernel source changes; PoC ran against the stock guest kernel.
Verified recommended fix
In pps_fetch_timeout(), clamp a computed zero tick count to 1 so any finite timeout still arms a tsleep timeout (see fix.diff).
Verdict
REPRODUCED (behavioral): pps_fetch_timeout() inverts a normalized finite sub-tick timeout (tv_sec==0, 0<tv_nsec<nstick) into an unbounded sleep β tstohz_low() returns 0 (kern_clock.c:1272) and tsleep(..., timo=0) arms no timeout (kern_synch.c:691) β verified live: PPS_IOC_FETCH with timeout {0 s, 1 ns} on /dev/ttyd0 was still asleep in wchan 'ppsfch' at t=12 s and only kill -9 reaped it, while control runs behave normally. IMPACT: none (Info) β on the default install PPS devices are root-only (ttyd0 = 0600), the hang is confined to the caller's own signal-killable thread. The suspected amplification β sioioctl() holding the global tty_token across pps_ioctl (sio.c:294/2165) making this a system-wide tty DoS β was tested and REFUTED: lwkt_switch() releases all tokens of a blocking thread (lwkt_thread.c:636-643, lwkt_relalltokens), and a concurrent PPS_IOC_GETCAP (which needs tty_token) completed in 0.000 s while the FETCH child slept in ppsfch.
No comments yet.