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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5100 Β· 11 files
FileTypeDescriptionSize
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 to timeout microseconds β€” max 10000us via JOY_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 moves nanotime() backward, while end = start + timeout stays fixed; timespeccmp(&t, &end, <) keeps the loop spinning until the wall clock again passes end. 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-reads timeout, 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.

VERDICT.md
↓ download raw

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() calls crit_enter() (joy.c:190) and then busy-loops (joy.c:200-211) reading the port and calling nanotime(&t) until the wall clock passes end = start + timeout (computed at joy.c:193-196).
  • nanotime() is realtime β€” it adds the global basetime (sys/kern/kern_clock.c:1562-1587, tsp->tv_sec += bt->tv_sec at 1581), and basetime is exactly what settimeofday()/NTP steps change.
  • Therefore a backward clock step during the loop moves the sampled t backward while end stays 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-reads timeout and has no other deadline.
  • Baseline behavior even without a step: every read masks interrupts on its CPU for up to 10 ms (JOY_SETTIMEOUT max, 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

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 no→ patch + rebuild →patched clean

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

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.