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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2212 Β· 18 files
FileTypeDescriptionSize
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 build
  • run.sh / run_fixed.log-companion run_fixed.sh β€” test drivers
  • ttm_module_fixed.c β€” pristine+fix.diff (sha256 4faabe2f…)
  • fix.diff β€” the verified one-hunk fix
  • build.log / fix_build.log β€” FULL untrimmed kernel builds
  • run.log β€” FULL baseline runtime output (decisive)
  • run_fixed.log β€” FULL fixed runtime output
  • trigger.out β€” baseline trigger process stdout (no exit line)
  • dmesg.txt β€” full dmesg spanning baseline and fixed eras
  • env.txt β€” guest environment
  • VERDICT.md β€” full narrative
  • manifest.json / verdict.json β€” machine-readable
VERDICT.md
↓ download raw

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 whole sys/ 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 .release hookup (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_name is a static inline returning 0 (sys/dev/drm/include/linux/device.h:106-110) and drm_class_device_register is a shipped no-op returning 0 (sys/dev/drm/drm_sysfs.c:60-63). drm_class_device_unregister is an empty body (drm_sysfs.c:73-75) β€” the DF port's device model never invokes any release.
  • wait_event maps 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) and tsleep(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) is SYSUNINIT(ttm_exit, SI_SUB_DRIVERS, SI_ORDER_SECOND, ...) (sys/dev/drm/include/linux/module.h:60-61), run by linker_file_sysuninit() β€” sorted descending, executed in the caller's thread (kern_linker.c:188-231, invoked from linker_file_unload at :535 after dropping llf_lock at :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 verbatim linux_wait.c/linux_sched.c (lkpi wait/sched support; the lkpi current macro lazily allocates the task_struct via linux_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=1 calls the REAL ttm_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 in p_siglist).
  • Disclosed jig: a SYSUNINIT(..., SI_ORDER_THIRD) would release the wait before ttm_exit's SI_ORDER_SECOND in any module-style uninit walk β€” inert here because DF never walks built-in sysuninit_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 878 was sent (kernel kill9 marker set at tick 5963); the kernel-side samples show sigkill_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.out contains no exit line β€” the sysctl(8) process never returned from the kernel.
  • exit_ms=-1, exit_done=0 for 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_lock is 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_LINKED cleared before the blocking sysuninit, module never leaves linker_files cleanly).
  • 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fixed: 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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Tue Oct 20 22:05:26 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/DFV2212 x86_64 (fix_build.log rc=0 under -Werror)

Confirmed kernel references

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.