joyread() busy-loops inside crit_enter() against WALL-CLOCK nanotime() β backward clock steps extend an interrupt-masked critical section beyond the configured timeout
Summary
The deadline uses the settimeofday/NTP-steppable wall clock inside an interrupt-masked crit section - a backward step keeps the CPU spinning ~step-seconds past the <=10ms timeout (deferred IPIs/timers); baseline is a 10ms masked spin per read even unstepped. Root-gated. nanouptime monotonic swap fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5100 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 2.7 KB | β raw | |
| joy_crit_loop.c | β | 1.9 KB | view raw | |
| build.sh | β | 100 B | view raw | |
| run.sh | β | 96 B | view raw | |
| build.log | β | 98 B | view raw | |
| run.log | β | 553 B | view raw | |
| env.txt | β | 887 B | view raw | |
| fix.diff | β | 547 B | view raw | |
| manifest.json | β | 994 B | view raw | |
| verdict.json | β | 2.9 KB | view raw |
DF-5100 β joyread() spins inside crit_enter() against WALL-CLOCK nanotime()
What
joyread() enters a critical section (crit_enter(), joy.c:190) and
busy-loops reading the game port until wall-clock nanotime() passes
start + timeout (joy.c:192-211), then crit_exit() (joy.c:213).
nanotime() is realtime: it adds the global basetime, which
settimeofday()/NTP steps mutate (sys/kern/kern_clock.c:1562-1587,
specifically tsp->tv_sec += bt->tv_sec at kern_clock.c:1581).
Defect mechanics
- By design (1995 ISA driver): each
read()masks interrupts and preemption on its CPU for up totimeoutmicroseconds β max 10000us viaJOY_SETTIMEOUT(joyioctl,joy.c:243). That alone is a 10ms interrupt-masked spin per read. - The bug: the loop deadline is in wall time. A backward clock step
(root
settimeofday(2), or an NTP daemon step β ntpd runs as root and steps by default) while a reader is inside the loop movesnanotime()backward, whileend = start + timeoutstays fixed;timespeccmp(&t, &end, <)keeps the loop spinning until the wall clock again passesend. A backward step of N seconds holds that CPU in a critical section (interrupts masked, preemption blocked, IPIs deferred) for ~N extra seconds. Nothing in the loop re-readstimeout, and the only exits are the wall-clock comparison and both-axis completion.
Security impact
Root-gated (/dev/joy* is 0600 root, joy.c:133): an unprivileged user
cannot trigger it, and root alone can already stall the machine. The
realistic scenario is root running a joystick reader while the time
daemon steps the clock β a latent local DoS / latency hazard. Filed as
Info hardening; the fix is to use monotonic time (nanouptime(),
sys/sys/time.h:185) for the measurement loop.
Build / run (as executed on the audit guest)
cc -O2 -Wall -o joy_crit_loop joy_crit_loop.c # -> BUILD_OK, no warnings ./joy_crit_loop
Expected on hardware with /dev/joy0: five read N: ~10000 us lines,
each an interrupt-masked spin; a concurrent date -v-N-style backward
settimeofday() extends individual reads by ~N seconds.
Observed on this guest: exit 2, open /dev/joy0: No such file or directory
(errno=2) β joy(4) never attaches (no ISA gameport; kldload joy alone
creates no node). See run.log, env.txt.
Fix
Swap nanotime() β nanouptime() at joy.c:193 and joy.c:204
(see fix.diff). Monotonic time is the correct base for a bounded
busy-wait deadline.
DF-5100 VERDICT
Status: inconclusive (not runtime-exercisable on this guest) β mechanism proven from source; Info hardening finding, PoC skipped per contract.
Reproduced?
No β same environment blocker as DF-5099: joy(4) is module-only and the
guest has no ISA gameport, so /dev/joy0 does not exist (ENOENT at
open(), run.log). The trigger builds cleanly on the guest (cc 8.3,
-Wall, zero diagnostics).
Why the defect is real anyway (source proof)
joyread()callscrit_enter()(joy.c:190) and then busy-loops (joy.c:200-211) reading the port and callingnanotime(&t)until the wall clock passesend = start + timeout(computed atjoy.c:193-196).nanotime()is realtime β it adds the globalbasetime(sys/kern/kern_clock.c:1562-1587,tsp->tv_sec += bt->tv_secat 1581), andbasetimeis exactly whatsettimeofday()/NTP steps change.- Therefore a backward clock step during the loop moves the sampled
tbackward whileendstays fixed in wall time;timespeccmp(&t,&end,<)keeps the loop spinning until wall time catches up β holding the CPU in a critical section (interrupts masked, preemption and IPIs deferred) for up to ~step-magnitude seconds. The loop never re-readstimeoutand has no other deadline. - Baseline behavior even without a step: every read masks interrupts on
its CPU for up to 10 ms (
JOY_SETTIMEOUTmax,joy.c:243).
Impact ceiling
Root-gated (joy.c:133, node 0600 root): only root can hold the device
open, and root can stall the system by other means. The realistic risk is
a root joystick reader combined with an NTP backward step β a latent local
DoS/latency hazard. Info severity.
Why no fix validation
Same as DF-5099: the guest kernel never instantiates the device, so
neither baseline nor patched behavior is observable. fix.diff
(nanotime β nanouptime at the two call sites; nanouptime exists at
sys/sys/time.h:185) is correct by construction β the monotonic clock
makes the deadline immune to wall-clock steps β but was not kernel-built.
Kernel references
- sys/dev/misc/joy/joy.c:190-213 (crit_enter + wall-clock bounded busy loop)
- sys/dev/misc/joy/joy.c:193,195-196,204 (nanotime use / deadline math)
- sys/kern/kern_clock.c:1562-1587 (nanotime = realtime, adds stepped basetime)
- sys/dev/misc/joy/joy.c:243 (timeout capped 1..10000 us)
- sys/sys/time.h:185 (nanouptime β the monotonic fix)
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 is git-apply-clean and correct by construction (monotonic deadline immune to wall-clock steps), but behavioral validation is impossible on this guest: the joy driver never attaches, so neither baseline nor patched kernel executes joyread().
fix.diff; run.log (device absent on guest); git apply --check output in session log.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (wall-clock proof: joy.c loop vs kern_clock.c basetime); run.log (ENOENT at open; kldload joy creates no node); build.log (clean -Wall build on guest cc 8.3); env.txt (no gameport hardware); fix.diff (nanotime->nanouptime, git-apply-clean).
PoC changes
None material β trigger written fresh for this pass; guest-built heredoc trimmed comments only (functionally identical to pack source).
Verified recommended fix
Use monotonic nanouptime() instead of nanotime() for the joyread() timing loop (joy.c:193,204); nanouptime exists at sys/sys/time.h:185 (see fix.diff).
Verdict
Mechanism proven from source: joyread() spins inside crit_enter() (joy.c:190) until WALL-CLOCK nanotime() passes start+timeout (joy.c:193-204); nanotime() adds the settimeofday/NTP-steppable basetime (kern_clock.c:1579-1586), so a backward clock step during the loop extends the interrupt-masked critical section by roughly the step magnitude, and even without a step every read masks interrupts on its CPU for up to 10ms (JOY_SETTIMEOUT cap, joy.c:243). Not runtime-exercisable on this guest: joy(4) attaches only to ISA/PnP gameport hardware β /dev/joy* absent even after kldload joy; trigger exits 2 at open() ENOENT (run.log). Impact ceiling Info: node is 0600 root-only (joy.c:133), so the realistic scenario is a root joystick reader plus an NTP backward step β a latent local DoS/latency hazard, not an unprivileged primitive. PoC skipped per contract for Info hardening findings; guest evidence captured instead.
No comments yet.