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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4300 Β· 5 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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)

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

fix.diff authored (interlocked sleep); not built (trigger not reproducible)

fix.diff
↓ fix.diffper-fix-DF-4300

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.