vkbd_dev_close check-then-sleep TASK handshake can miss the drain task's wakeup and pin the kernel mplock indefinitely (system-wide device-I/O stall until signalled)
Summary
close() waits with a while-flags tsleep while the swi task clears the flag and wakes - no interlock between the test and sleep insertion, so a wakeup landing between is lost: close sleeps forever. Because d_close runs under the mplock (non-MPSAFE dev_ops) and tsleep does not release it, a hit pins the mplock, stalling every non-MPSAFE dev_d*() until signalled. ~90,000 open/write/close cycles racing the task never hit it - the window is real but empirically very narrow (honest not_reproduced with the hammer harness preserved). NOTE: the marquee ring-corruption theory was REFUTED: the no-op VKBD_LOCK macros are safe because the mplock serializes everything (240M writes verified clean); the two fuzz 'crashes' were Ctrl-Alt-Del injection through the keystroke payload - by-design function of a root-only node. tsleep_interlock fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4300 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 1.8 KB | β raw | |
| fix.diff | β | 421 B | view raw | |
| manifest.json | β | 520 B | view raw | |
| verdict.json | β | 1.8 KB | view raw |
DF-4300 β vkbd_dev_close check-then-sleep TASK handshake can miss the drain task's wakeup while pinning the kernel mplock
Finding
vkbd_dev_close() (sys/dev/misc/vkbd/vkbd.c:307-308) waits for the drain
task with a plain check-then-sleep loop:
while (state->ks_flags & TASK)
VKBD_SLEEP(state, ks_task, "vkbdc", 0); /* tsleep, PCATCH */
and the task side (vkbd_dev_intr, vkbd.c:627-632) clears the flag and
wakes the sleeper after draining:
kbd_intr(kbd, NULL);
state->ks_flags &= ~TASK;
wakeup(&state->ks_task);
There is no interlock (no tsleep_interlock/critsec between the flag test and the sleep), so a wakeup that lands between the close path's flag test and its tsleep() insertion is lost: close sleeps indefinitely.
Aggravating factor: vkbd's dev_ops is not D_MPSAFE (vkbd.c:179-190,
flags 0), so d_close is dispatched under the kernel mplock
(sys/kern/kern_device.c:174-189, dev_needmplock() at :119-121), and
DragonFly's tsleep() does not release the mplock
(sys/kern/kern_synch.c β no mplock handling). A close that blocks here
pins the mplock, stalling every other non-MPSAFE dev_d*() call in the
system until the sleeper receives a signal (PCATCH) or forever.
Status
Not reproduced live: ~90,000 open/write/close cycles racing the drain
task against close (vkbd_wedge.c, log in ../DF-4302 session) never
observed a stuck close β the checkβtsleep window is empirically very
narrow. The race is code-real (no interlock) but stochastic; filed as
speculative.
Fix
Use an interlocked sleep (test-and-sleep under a critical section /
tsleep_interlock pattern), or have the task hold off clearing TASK until
it can re-check sleepers; simplest: perform the wait with the standard
tsleep_interlock()+tsleep() pairing around the flag check.
DF-4300 verdict
Reproduced?
Not reproduced (not_reproduced β honest stochastic outcome, not a
refutation of the code pattern).
Empirical hammer: vkbd_wedge.c ran ~90,000 open("/dev/vkbdctl") β
write(2 scancodes) β close() cycles in forked children so that close
always raced the just-enqueued drain task (the exact precondition for
the missed wakeup). No close blocked; the window between the
while (state->ks_flags & TASK) test (vkbd.c:307) and tsleep insertion
proved too narrow to hit in that many attempts on this 6-vCPU guest.
Root cause (path:line)
sys/dev/misc/vkbd/vkbd.c:307-308β check-then-sleep, no interlock.sys/dev/misc/vkbd/vkbd.c:627-632β task clears TASK + wakeup unconditionally afterkbd_intr().sys/kern/kern_device.c:119-121,174-189β vkbd dev_ops flags 0 βd_closedispatched under the mplock.sys/kern/kern_synch.cβtsleep()contains no mplock release, so a blocked close holds the mplock.
Impact if hit
The closing thread sleeps holding the mplock β all non-MPSAFE device entry points system-wide block β broad device-I/O stall (disk/net on any non-MPSAFE drivers), recoverable only by signalling the sleeper (PCATCH makes tsleep return; the loop then re-checks TASK, which is now clear, and close completes). Root-triggered (the /dev/vkbdctl node is 0600 root:wheel), narrow window β Low severity, speculative confidence.
Fix
Interlocked sleep around the TASK check (see fix.diff).
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 (interlocked sleep); not built (trigger not reproducible)
fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md; race harness vkbd_wedge.c (see ../logs) ran ~90k cycles with no stuck close
PoC changes
Harness built and run; race window too narrow to hit
Verified recommended fix
Interlock the TASK check and tsleep (tsleep_interlock pattern) in vkbd_dev_close.
Verdict
vkbd_dev_close() waits for the drain task with an un-interlocked check-then-sleep (vkbd.c:307-308 vs task-side clear+wakeup at :629-630); a wakeup landing between the flag test and tsleep() is lost and close sleeps forever. Because vkbd's dev_ops is not D_MPSAFE, d_close runs under the kernel mplock and tsleep does not release it, so a hit pins the mplock system-wide until a signal. ~90k live open/write/close race cycles did not trigger it (window empirically very narrow) β filed speculative.
No comments yet.