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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5834 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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)

  1. sys/kern/kern_clock.c:1672 β€” PPS_IOC_FETCH enters the timeout path for any nonzero timeout: if (fapi->timeout.tv_sec != 0 || fapi->timeout.tv_nsec != 0).
  2. 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), so to = tstohz_low({0,1}).
  3. sys/kern/kern_clock.c:1272 β€” tstohz_low: ticks = (int)(sec * hz + (u_long)ts->tv_nsec / nstick) = (int)(0 + 1/10000000) = 0.
  4. sys/kern/kern_clock.c:1626 β€” tsleep(pps, PCATCH, "ppsfch", 0).
  5. sys/kern/kern_synch.c:691 β€” if (timo) guards the timeout callout, so timo == 0 means "no timeout": the thread sleeps until wakeup(pps) (a PPS edge β€” none on an idle serial line) or a signal (PCATCH). EWOULDBLOCK never occurs, so the tv_sec < 0 β†’ continue retry 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; parent PPS_IOC_GETCAP elapsed 0.000 s. βœ”
  • 8 s finite wedge (ppslockdemo 8): child issued PPS_IOC_FETCH with {8,0}; mid-wedge ps shows the child in state S4, wchan ppsfch; 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 wchan ppsfch with no timeout armed; only kill -9 reaped 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's PPS_IOC_GETCAP β€” which must acquire tty_token through the same sioioctl() 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_testable
baseline reproduced→ patch + rebuild →patched clean

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

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.