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

wait_scrn_saver_stop sleeps holding vga_token while its only wake path must take vga_token: CONS_SET/KDSETMODE/VT_ACTIVATE issued while an LKM screen saver has the console blanked deadlocks the console subsystem, and killing the caller leaves scrn_blanked permanently stuck (saver module unloadable forever, later saver-stop waiters sleep forever)

Summary

Lock inversion: the sleeper holds vga_token across tsleep(&scrn_blanked) while the wake path (scrn_timer->stop_scrn_saver->restore->set_mode) requires vga_token - and the kill path leaves the count stuck >=1 forever (a self-sustaining wedge until reboot). REPRODUCED deterministically on a fresh vidconsole boot: child blocked >10s in CONS_SET, scrn_blanked==1 permanent, kldunload times out. Realistic X-startup-after-idle trigger; privileged (0600 ttyv). Drop-and-reacquire-own-token-reference fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5195 Β· 9 files
FileTypeDescriptionSize
deadlock_probe.c β€” 2.8 KB view raw
kread.c β€” 798 B view raw
build.sh β€” 123 B view raw
run.sh β€” 136 B view raw
run.log β€” 1.1 KB view raw
discovery_ps.txt β€” 931 B view raw
fix.diff β€” 1.2 KB view raw
verdict.json β€” 5.0 KB view raw
env.txt β€” 629 B view raw

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff authored (sc_clean_up vga_token drop/reacquire around the saver-stop wait) but not built or run: validating it requires a guest kernel rebuild, and per the audit contract fix validation is mandatory only for memory-corruption findings. The proposed fix is LIFO-safe for all callers because sc_clean_up acquires its own token reference at entry; residual risk is a widened (non-deadlocking) race window while the token is dropped, strictly better than the deterministic deadlock.

fix.diff; deadlock baseline in run.log
↓ fix.diffper-fix-DF-5195

Confirmed kernel references

Detail

Exploit chain

root loads any LKM saver module once (documented loader.conf configuration) -> console blanks after idle (scrn_blank_time defaults to 300s) -> any process with a /dev/ttyvN fd issues CONS_SET/KDSETMODE/VT_ACTIVATE while blanked -> kernel deadlock (ioctl never returns; kill leaves scrn_blanked permanently stuck, module unload permanently hangs, subsequent console ioctls that wait on the saver sleep forever holding vga_token and block the scrn_timer stop retry) -> console subsystem wedged until reboot. Ceiling: kernel-console DoS; no privilege escalation path (no memory corruption).

Evidence (decisive lines)

run.log (fresh vm.sh reset with-src boot: child blocked 10s in CONS_SET -> DEADLOCK REPRODUCED -> SIGKILL -> 12s later kread shows scrn_blanked=1 stuck -> kldunload timeout RC=124); discovery_ps.txt (wchan 'scrsav' for the hung ioctl and for a later 'vidcontrol -p' waiter; /dev/kmem dumps showing scrn_blanked=1, SC_SCRN_BLANKED SET, saver_mode=CONS_LKM_SAVER); deadlock_probe.c (deterministic reproducer); kread.c (live statics reader).

PoC changes

n/a (finding and PoC originated in this run; no seeded draft to fix)

Verified recommended fix

sc_clean_up(): drop sc_clean_up's own vga_token reference around wait_scrn_saver_stop() and re-acquire it, so the saver-stop wake path (stop_scrn_saver -> restore_scrn_saver_mode(TRUE) -> set_mode) can take the token; LIFO-safe because the dropped reference is the innermost one on the thread's token stack.

Verdict

Deterministically reproduced on a fresh-boot QEMU guest (vidconsole kernel console + snake_saver.ko loaded, blank timer default): a CONS_SET mode-change ioctl issued while the LKM screen saver has the console blanked never returns -- the calling thread sleeps on &scrn_blanked ('scrsav') holding vga_token (scioctl syscons.c:983 / sc_set_text_mode scvidctl.c:68 -> sc_clean_up -> wait_scrn_saver_stop non-unlock branch), while the only wake path (scrn_timer -> stop_scrn_saver -> scsplash_saver(FALSE) -> restore_scrn_saver_mode(TRUE) -> set_mode) must lwkt_gettoken(&vga_token) at syscons.c:4171 -- a token inversion deadlock. Killing the hung process does not restore consistency: scrn_blanked stays 1 and SC_SCRN_BLANKED stays set forever (verified via /dev/kmem), the saver module becomes permanently unloadable (kldunload hangs, RC=124), and every later saver-stop waiter sleeps forever while holding vga_token, wedging the console subsystem and the scrn_timer callout until reboot. Not memory corruption: a privileged-trigger local kernel DoS with permanent state corruption (realistic sequence: console idles 5 min -> blank -> X/vidcontrol starts).