ttm_exit() deadlocks forever in uninterruptible wait_event on condition that can never become true
Summary
ttm_exit() ends with wait_event(exit_q atomic_read(&device_released)==1) but the only code that ever sets device_released=1 and wakes exit_q (the .release callback ttm_drm_class_device_release) is wrapped in #if 0 (lines 40-53) and never compiled. After successful ttm_init the success path explicitly sets device_released=0 (line 77) and no other kernel code path can ever flip it to 1 (verified wake_up_all(&exit_q) and atomic_set(&device_released 1) appear nowhere else in sys/). At module unload (kldunload) and at system shutdown via SYSUNINIT the calling thread enters TASK_UNINTERRUPTIBLE tsleep with no PCATCH and loops forever in hz-timeout retry inside __wait_event_common becoming unkillable D-state process that wedges module linker. Root cause: Linux upstream TTM design relies on device model device_unregister drops last ref and struct device .release callback sets device_released=1 and calls wake_up_all. DragonFly port broke this in two ways: (1) disabled .release via #if 0 (2) drm_class_device_unregister is no-op never drops ref. Author flagged uncertainty in comment lines 93-96 (Not sure this is 100% needed). [VERIFIED pass2: reproduced - the kldunload thread running verbatim ttm_exit blocks D0 wchan lwe forever, SIGKILL posted and IGNORED 16 of 16 samples over 80s, device_released stuck 0; no-other-setter proven by tree grep, shutdown arm REFUTED - DF never runs module sysuninits at shutdown so kldunload is the sole trigger; refined wedge is unkillable thread plus half-torn module not a global lock; delete-the-dead-wait fix-validated - exit returns in 0 ms, 500 cycles clean]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2212 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 8.3 KB | β raw | |
| df_ttm2212.c | β | 10.8 KB | view raw | |
| build.sh | β | 258 B | view raw | |
| run.sh | β | 126 B | view raw | |
| run_fixed.sh | β | 1.2 KB | view raw | |
| ttm_module_fixed.c | β | 2.7 KB | view raw | |
| fix.diff | β | 375 B | view raw | |
| build.log | β | 5.6 MB | β download | |
| fix_build.log | β | 5.5 MB | β download | |
| run.log | β | 5.4 KB | view raw | |
| run_fixed.log | β | 1.7 KB | view raw | |
| trigger.out | β | 24 B | view raw | |
| dmesg.txt | β | 50.2 KB | view raw | |
| env.txt | β | 577 B | view raw | |
| manifest.json | β | 2.8 KB | view raw | |
| verdict.json | β | 7.5 KB | view raw | |
| SHA256SUMS.artifacts | β | 857 B | β download |
DF-2212 β ttm_exit() uninterruptible deadlock (dead wait_event)
sys/dev/drm/ttm/ttm_module.c:98 β ttm_exit() ends with
wait_event(exit_q, atomic_read(&device_released) == 1) but the only
compiled setter of device_released=1 is #if 0'd out; the init
success path zeroes it and success is guaranteed (dev_set_name and
drm_class_device_register are no-ops returning 0 on DF). The thread
that runs ttm_exit at module unload β the kldunload(2) thread via
linker_file_sysuninit (kern_linker.c:535) β sleeps uninterruptibly
forever: D-state, SIGKILL ignored, module half-unloaded.
Verdict: reproduced (baseline: 80 s watchdog, 16/16 samples blocked, sigkill pending+ignored, D0/wchan "lwe"; fixed: exit returns in 0 ms, 500 clean init/exit cycles). Impact: unkillable-thread DoS wedge, root-triggered (kldunload); no unprivileged reach, no memory corruption. Severity Low.
Reproduce (on the DragonFly audit guest)
# 1. push the pack to the guest (or use the copies under /root/poc/DF-2212)
# df_ttm2212.c build.sh run.sh run_fixed.sh ttm_module_fixed.c fix.diff
# 2. build + install the baseline harness kernel (pristine ttm_module.c,
# sha256-gated 56d64f99e2ee74fee71bdb775463f51cbc6a1d3f9096f2da25af60b55ab26b8e)
sh build.sh baseline # make -j6 nativekernel KERNCONF=DFV2212
cp /usr/obj/usr/src/sys/DFV2212/kernel.stripped /boot/kernel/kernel
cp /usr/obj/usr/src/sys/DFV2212/kernel.debug /boot/kernel/kernel.debug
shutdown -r now
# 3. run the baseline test (~100 s)
sh run.sh # -> run.log: D0 state, wchan lwe,
# kill -9 ignored, watchdog verdict
# 4. apply the fix, rebuild, rerun
cp ttm_module_fixed.c /usr/src/sys/dev/drm/ttm/ttm_module.c # == fix.diff applied
sh build.sh fixed
cp /usr/obj/usr/src/sys/DFV2212/kernel.stripped /boot/kernel/kernel
cp /usr/obj/usr/src/sys/DFV2212/kernel.debug /boot/kernel/kernel.debug
shutdown -r now
sh run_fixed.sh # -> run_fixed.log: returns in 0 ms,
# 500 cycles, nothing stuck
Success criterion (baseline): ps shows the trigger process in state
D0 with wchan lwe at t=3 s, 6 s, 33 s and 93 s after kill -9;
kernel log shows 16/16 watchdog samples with TDF_TSLEEPQ=YES
wmesg="lwe" device_released=0 sigkill_pending=1 and the final WATCHDOG
VERDICT line; the trigger sysctl never returns. Success criterion
(fixed): ttm_exit() RETURNED after 0 ms, 500 init/exit cycles
complete, no stuck processes.
Rig
df_ttm2212.c is compiled into the kernel (config DFV2212 =
X86_64_GENERIC + device dfverify) and #includes the REAL
<dev/drm/ttm/ttm_module.c> (sha256-gated by build.sh) plus verbatim
lkpi linux_wait.c/linux_sched.c. The two drm_sysfs externs are
provided with exactly the shipped semantics (drm_sysfs.c:60-75).
module_init(ttm_init) runs at boot (control); hw.df_ttm2212.trigger
calls the real ttm_exit() in the calling thread (kldunload-thread
simulation); a 5 s callout watchdog samples the blocked thread.
Files
df_ttm2212.cβ harness (verbatim interposition)build.shβ sha256-gated baseline/fixed kernel buildrun.sh/run_fixed.log-companionrun_fixed.shβ test driversttm_module_fixed.cβ pristine+fix.diff (sha256 4faabe2fβ¦)fix.diffβ the verified one-hunk fixbuild.log/fix_build.logβ FULL untrimmed kernel buildsrun.logβ FULL baseline runtime output (decisive)run_fixed.logβ FULL fixed runtime outputtrigger.outβ baseline trigger process stdout (no exit line)dmesg.txtβ full dmesg spanning baseline and fixed erasenv.txtβ guest environmentVERDICT.mdβ full narrativemanifest.json/verdict.jsonβ machine-readable
DF-2212 β VERDICT
File: sys/dev/drm/ttm/ttm_module.c (lines 37-60, 77, 84-85, 89-99)
Claim: ttm_exit() deadlocks forever in an uninterruptible
wait_event on a condition that can never become true; at
kldunload/SYSUNINIT the calling thread waits unkillably.
Severity as filed: Low (root-triggered DoS wedge). Status: REPRODUCED.
Bottom line
Reproduced on the QEMU guest with the real, verbatim production code
compiled into a harness kernel (rig class: verbatim interposition,
kernel-compiled because runtime kldload of drm GPFs on this guest).
The thread that runs the real ttm_exit() β the same function
linker_file_sysuninit() (sys/kern/kern_linker.c:535) runs in the
kldunload(2) syscall thread of drm.ko β blocks in uninterruptible sleep
forever: D-state, kill -9 delivered and ignored (pending in
p_siglist for 80+ s of watchdog sampling), device_released stuck at
0 with no compiled code path able to set it to 1. The one-line-class
fix (delete the dead wait) makes exit return in 0 ms with 500 clean
init/exit cycles and no other behavior change. Impact is a
root-triggered unkillable-thread/module-unload wedge (DoS); there is no
memory-corruption primitive and no privilege boundary crossed β the
privesc directive is closed: not applicable.
Why the condition can never become true (source certainty)
static atomic_t device_released;β ttm_module.c:38. Grep of the wholesys/tree: the ONLY writes of the value 1 are ttm_module.c:50 (ttm_drm_class_device_release) and ttm_module.c:84 (init error path).- ttm_module.c:40-53: the release function and its
.releasehookup (ttm_module.c:56-59) are inside#if 0β never compiled. - The success path zeroes it: ttm_module.c:77
(
atomic_set(&device_released, 0)), and success is guaranteed on DF:dev_set_nameis a static inline returning 0 (sys/dev/drm/include/linux/device.h:106-110) anddrm_class_device_registeris a shipped no-op returning 0 (sys/dev/drm/drm_sysfs.c:60-63).drm_class_device_unregisteris an empty body (drm_sysfs.c:73-75) β the DF port's device model never invokes any release. wait_eventmaps to__wait_event_common(wq, cond, 0, 0, false)(sys/dev/drm/include/linux/wait.h:171-172): flags 0 β no PCATCH βTASK_UNINTERRUPTIBLE(wait.h:121) andtsleep(current, PINTERLOCKED, "lwe", hz)in a loop that only exits when the condition holds (wait.h:124-151). Signals do not interrupt it.module_exit(ttm_exit)isSYSUNINIT(ttm_exit, SI_SUB_DRIVERS, SI_ORDER_SECOND, ...)(sys/dev/drm/include/linux/module.h:60-61), run bylinker_file_sysuninit()β sorted descending, executed in the caller's thread (kern_linker.c:188-231, invoked fromlinker_file_unloadat :535 after droppingllf_lockat :533-534).
Honest refinement of the filed claim: DF shutdown does not run
module SYSUNINITs at all (kern_shutdown.c contains no linker/sysuninit
references; only kldunload paths invoke linker_file_sysuninit). The
"or shutdown" arm of the claim is therefore not the live path on DF;
the real-world trigger is kldunload drm (or a dependency/forced
unload cascade), which requires root. Consequence on a real
drm-bearing system: the unloading thread is unkillable (D-state
forever), the drm module is left half-torn (LINKER_FILE_LINKED
cleared, sysuninit incomplete β kern_linker.c:531-535), and the
operation never completes. No unprivileged reach; Low severity is
correct.
Rig (verbatim interposition, kernel-compiled)
- Harness
df_ttm2212.c#include <dev/drm/ttm/ttm_module.c>β the REAL tree file, sha256-gated (56d64f99e2ee74fee71bdb775463f51cbc6a1d3f9096f2da25af60b55ab26b8e, byte-identical guest/audited trees), plus verbatimlinux_wait.c/linux_sched.c(lkpi wait/sched support; the lkpicurrentmacro lazily allocates the task_struct vialinux_task_alloc, linux_sched.c:59). The two externs ttm calls are provided with EXACTLY the shipped semantics of drm_sysfs.c:60-75 (return 0 / empty) plus observation counters β the only additions. - Kernel config
DFV2212= X86_64_GENERIC +device dfverify(INVARIANTS + DDB preserved).module_init(ttm_init)fires at boot via SYSINIT β the free control. hw.df_ttm2212.trigger=1calls the REALttm_exit()in the sysctl thread β same thread-context as the kldunload path.- A 5-second callout watchdog on another CPU samples the blocked
thread (
TDF_TSLEEPQ,td_wmesg,td_wchan,device_released, SIGKILL-pending bit inp_siglist). - Disclosed jig: a
SYSUNINIT(..., SI_ORDER_THIRD)would release the wait beforettm_exit'sSI_ORDER_SECONDin any module-style uninit walk β inert here because DF never walks built-insysuninit_set; it exists so the rig cannot wedge a shutdown path even by accident.
Baseline results (pristine tree file) β run.log, dmesg.txt
df_ttm2212: BOOT CHECK: ... register_calls=1 device_released=0
(register>0 + value 0 => SUCCESS path => a later ttm_exit() can
never pass its wait)
df_ttm2212: TRIGGER pid=878 (sysctl): calling the REAL ttm_exit() ...
Trace beginning at frame ... <- print_backtrace at wait entry
userland_sysctl() ... sys___sysctl() ... syscall2() ...
ps: 878 D0 lwe sysctl <- t=3s, 6s, 33s, 93s: D-STATE,
wchan "lwe" (= wait.h:135 tsleep wmesg)
df_ttm2212: WATCHDOG sample 1..16/16 t+5s..t+80s:
TDF_TSLEEPQ=YES wmesg="lwe" wchan=0x... device_released=0
sigkill_pending=1 kill9_marker=set
df_ttm2212: WATCHDOG VERDICT after 16 samples (~80s): ttm_exit()
thread STILL blocked; device_released=0 and no compiled code path
in sys/ can ever set it to 1 -> wait_event can never return; sleep
is uninterruptible (no PCATCH)
kill -9 878was sent (kernel kill9 marker set at tick 5963); the kernel-side samples showsigkill_pending=1β the signal is posted and ignored: the process survives SIGKILL for the full 93 s observation (and beyond; it only dies with the box).trigger.outcontains no exit line β the sysctl(8) process never returned from the kernel.exit_ms=-1,exit_done=0for the whole run.- Cross-kernel corroboration: the warm-reboot msgbuf carried the baseline watchdog lines into the fixed kernel's dmesg (dmesg.txt holds both eras back-to-back).
Fixed results (tree file = pristine + fix.diff, sha256
4faabe2f6a7f28231bcee79db239110a4b909985a67a4f12d93ab62f715187b9) β run_fixed.log, dmesg.txt
df_ttm2212: ttm_exit() RETURNED after 0 ms (device_released=0) -
fixed kernel behaviour
df_ttm2212: CYCLE: 500 x (ttm_init(); ttm_exit()) completed, every
call returned
--- trigger: the sysctl command itself must COMPLETE now ---
0.07 real ... trigger rc=0
--- stuck sysctl procs: (none)
device_released is still 0 after the fixed exit β proving the wait
was removed (not that the condition somehow became true). Identical
kernel otherwise; boot check unchanged; guest healthy.
Fix
fix.diff (git-apply-able, verified to apply to the pristine tree and
to reproduce the tested fixed file byte-for-byte): delete the dead
wait_event and its "Not sure this is 100% needed" comment from
ttm_exit(). This is the maintainer-minimal shape the author himself
anticipated. exit_q/device_released and the init error-path wake
are deliberately left untouched so the patch stays one-hunk reviewable;
a follow-up cleanup could delete them together with the #if 0 block.
Checklist / negatives
- No unprivileged trigger exists (sysctls are root-only; the real path is kldunload(2) = privileged). Not memcorrupt; privesc N/A.
- "Wedges module linker": refined β
llf_lockis RELEASED around the sysuninit call (kern_linker.c:533-535), so the global linker lock is not held; the wedge is the unkillable unloader thread plus a half-torn module (LINKER_FILE_LINKEDcleared before the blocking sysuninit, module never leaveslinker_filescleanly). - Shutdown arm of the filed claim: killed by source reading (no sysuninit walk at shutdown on DF).
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
fixedfixed: baseline kernel (pristine tree file) blocked the calling thread forever with SIGKILL ignored; identical kernel with only fix.diff applied (tree file sha256 4faabe2f... verified before build) returned from ttm_exit in 0 ms, completed 500 init/exit cycles, left no stuck processes, boot behavior unchanged, device_released still 0 proving the wait itself was the defect; bad behavior GONE post-fix
fix.diff; fix_build.log (full untrimmed build, rc=0); run_fixed.log (0.07 real trigger, RETURNED after 0 ms, CYCLE 500 completed, none stuck); run.log (baseline hang for contrast); dmesg.txt (both eras)
Confirmed kernel references
- sys/dev/drm/ttm/ttm_module.c:37
- sys/dev/drm/ttm/ttm_module.c:38
- sys/dev/drm/ttm/ttm_module.c:40
- sys/dev/drm/ttm/ttm_module.c:48
- sys/dev/drm/ttm/ttm_module.c:50
- sys/dev/drm/ttm/ttm_module.c:56
- sys/dev/drm/ttm/ttm_module.c:77
- sys/dev/drm/ttm/ttm_module.c:84
- sys/dev/drm/ttm/ttm_module.c:98
- sys/dev/drm/include/linux/wait.h:110
- sys/dev/drm/include/linux/wait.h:121
- sys/dev/drm/include/linux/wait.h:130
- sys/dev/drm/include/linux/wait.h:135
- sys/dev/drm/include/linux/wait.h:171
- sys/dev/drm/include/linux/module.h:58
- sys/dev/drm/include/linux/module.h:60
- sys/dev/drm/include/linux/device.h:106
- sys/dev/drm/drm_sysfs.c:60
- sys/dev/drm/drm_sysfs.c:73
- sys/dev/drm/linux_sched.c:59
- sys/kern/kern_linker.c:188
- sys/kern/kern_linker.c:533
- sys/kern/kern_linker.c:535
Detail
Evidence (decisive lines)
run.log (decisive baseline output: ps D0/lwe snapshots at t=3/6/33/93s, status sysctls with exit_done=0 exit_ms=-1, kill -9 marker tick 5963, full 16-sample watchdog block and WATCHDOG VERDICT line, trigger.out shows no exit line); dmesg.txt (kernel-side TRIGGER backtrace print at wait entry: userland_sysctl/sys___sysctl/syscall2 - a user syscall thread, plus baseline watchdog era and fixed era side by side thanks to warm-reboot msgbuf survival); run_fixed.log (trigger completes 0.07 real, ttm_exit RETURNED after 0 ms, CYCLE 500 completed, none stuck); build.log and fix_build.log (full untrimmed -Werror kernel builds, rc=0); VERDICT.md (full narrative incl. the no-other-setter grep proof); fix.diff (one-hunk, git-apply-able, verified byte-exact against the tested fixed file)
PoC changes
No seeded pack existed; authored the whole rig per the directive. Runtime kldload of drm GPFs on this guest (established), so the rig compiles the verbatim code into a custom kernel: harness df_ttm2212.c includes the real tree file by path so the baseline vs fixed A/B exercises exactly the tree under test; lkpi linux_wait.c and linux_sched.c included verbatim to close the wait/sched symbols (the lkpi current macro needs linux_task_alloc); config(8) rejects digits in device names so the pseudo-device is dfverify, not dfv2212; scripts and logs live under /root/poc (tmpfs /tmp is wiped each boot); build.sh sha256-gates the tree file for both modes.
Verified recommended fix
Delete the dead wait_event (and its comment) from ttm_exit - the author comment itself doubts it; verified fix.diff removes exactly ttm_module.c:93-98
Verdict
REPRODUCED with the real production code on the QEMU guest via a verbatim-interposition kernel-compiled rig (kernel config DFV2212 = X86_64_GENERIC + device dfverify; harness df_ttm2212.c #includes the REAL sha256-gated sys/dev/drm/ttm/ttm_module.c, byte-identical guest/audited trees, plus verbatim lkpi linux_wait.c and linux_sched.c; the two drm_sysfs externs carry exactly the shipped no-op semantics of drm_sysfs.c:60-75). Control: the verbatim ttm_init ran at boot via SYSINIT (module.h:58-59), register_calls=1 and device_released=0 - the init SUCCESS path (ttm_module.c:77) - so the exit condition is dead from the start. Trigger: hw.df_ttm2212.trigger=1 called the real ttm_exit() in the calling user thread, which is the same thread-context linker_file_sysuninit (kern_linker.c:535) gives the kldunload(2) thread of drm.ko; the thread blocked FOREVER: ps state D0 with wchan lwe (the literal tsleep wmesg of wait_event at wait.h:135) at t=3s/6s/33s/93s, SIGKILL posted and pending in p_siglist in all 16 kernel-side watchdog samples over 80s yet never acted upon (unkillable), TDF_TSLEEPQ=YES, device_released=0 in every sample, the sysctl command never returned, and the watchdog verdict line records that no compiled code path in sys/ can set device_released=1 (the only setter is #if 0 at ttm_module.c:48-52; the init error-path setter at :84 cannot run because dev_set_name and drm_class_device_register are no-ops returning 0 on DF - device.h:106-110, drm_sysfs.c:60-63). The sleep is uninterruptible by construction: wait_event passes flags=0, no PCATCH, TASK_UNINTERRUPTIBLE (wait.h:110-172). FIXED KERNEL (same tree + fix.diff, sha256-gated 4faabe2f...): ttm_exit() RETURNED after 0 ms with device_released still 0 (proving the wait was removed, not the condition set), 500 x (ttm_init;ttm_exit) cycles all returned, no stuck processes, no other behavior change. Impact ceiling: root-triggered (kldunload drm / unload cascade) unkillable D-state thread plus a half-torn module (LINKER_FILE_LINKED already cleared before the blocking sysuninit, kern_linker.c:531-535) - a permanent DoS wedge of the unload operation on any drm-bearing system. Honest refinement of the filed claim: DF shutdown does NOT run module SYSUNINITs (kern_shutdown.c has no linker/sysuninit walk), so the shutdown arm of the claim is source-dead; kldunload is the live path. No unprivileged reach, no memory corruption, no privilege boundary crossed - privesc directive closed as not applicable; Low severity confirmed.
No comments yet.