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)
PoC verification
Evidence pack
findings/poc/DF-5195 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_testablefix.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
Confirmed kernel references
- sys/dev/misc/syscons/syscons.c:2720
- sys/dev/misc/syscons/syscons.c:2734
- sys/dev/misc/syscons/syscons.c:983
- sys/dev/misc/syscons/syscons.c:2691
- sys/dev/misc/syscons/syscons.c:2204
- sys/dev/misc/syscons/syscons.c:4171
- sys/dev/misc/syscons/syscons.c:3536
- sys/dev/misc/syscons/scvidctl.c:105
- sys/dev/misc/syscons/scvidctl.c:68
- sys/kern/lwkt_token.c:832
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).
No comments yet.