ttm_eu_reserve_buffers() duplicate-BO (-EALREADY/dups) handling is dead code on DragonFly: the DFly ww_mutex never returns -EALREADY, so duplicate BOs from an unprivileged CS list are silently recursively double-locked and ww ticket accounting desyncs
Summary
The dups branch codes against the Linux -EALREADY contract but the DF ww_mutex uses LK_CANRECURSE silent recursion - a duplicate GEM handle in a user CS list double-locks and ctx->acquired desyncs (first unlock NULLs ww->ctx; second skips). RUNTIME-PROVEN on stock guest (second_lock=0 vs Linux -37, acquired=1 after balanced unlocks, 3 runs); the reserve_shared-failure path via a user-controlled duplicate collapses into the DF-2136 permanent-lock wedge. __wwlock -EALREADY fix validated end-to-end on a rebuilt+reloaded drm.ko (second_lock=-37, RESULT=FIXED).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5138 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 7.3 KB | β raw | |
| df5138_wwproof.c | β | 4.4 KB | view raw | |
| Makefile | β | 327 B | β download | |
| build.sh | β | 125 B | view raw | |
| run.sh | β | 148 B | view raw | |
| build.log | β | 5.8 KB | view raw | |
| run.log | β | 519 B | view raw | |
| run.2.log | β | 533 B | view raw | |
| fix_run.log | β | 501 B | view raw | |
| fix_build.log | β | 124.9 KB | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| unload_hang.txt | β | 1007 B | view raw | |
| env.txt | β | 467 B | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 6.3 KB | view raw |
DF-5138 β ttm_eu_reserve_buffers dups handling is dead code on DragonFly
Files: sys/dev/drm/ttm/ttm_execbuf_util.c:119-124 (consumer of the
broken contract) / root cause in sys/dev/drm/linux_wwmutex.c (DFly's
ww_mutex).
What: ttm_eu_reserve_buffers() relies on __ttm_bo_reserve() returning
-EALREADY when a BO's reservation is already held by the same ticket, so a
duplicate BO (same GEM handle listed twice in a user CS submission β radeon
relocs chunk and amdgpu BO list both pass duplicates through without
dedupe) gets moved to the dups list and is locked exactly once. DragonFly's
ww_mutex NEVER returns -EALREADY: ww_mutex_init() uses LK_CANRECURSE
(linux_wwmutex.c:62) and lockmgr's same-thread recursive acquire silently
succeeds (kern_lock.c:125-143), so __wwlock returns 0 and re-bumps
ctx->acquired. The dups branch is dead code; the duplicate is silently
double-locked, and ww_mutex_unlock() (linux_wwmutex.c:188-202) decrements
ctx->acquired only once per resv (the first unlock NULLs ww->ctx, the
second skips the decrement) β the ticket accounting desyncs. A second
consumer already codes against the Linux contract: modeset_lock()
(drm_modeset_lock.c:270-276) explicitly maps -EALREADY to "we already hold
the lock".
Impact: success path is lock-count balanced (no corruption). Failure
paths desync: in the uh-oh/retry path the wedged accounting makes later
locks on the same ticket use the NOWAIT+tsleep route, and when
reservation_object_reserve_shared() fails on a duplicate's second
recursive episode, the reverse-backoff releases only the first episode β
the BO's reservation lock stays permanently held (DF-2136-class wedge,
reachable through a user-controlled duplicate list entry). Realistic
ceiling: local DoS on GPU-equipped systems. Low (hardware-gated,
conditional trigger), certain for the mechanism.
Reproduce (runtime, on the DFly guest β mechanism level)
The KLD df5138_wwproof.c performs the exact sequence
ttm_eu_reserve_buffers() performs for a duplicate BO (two
ww_mutex_lock() on the same resv with the same ticket), then the two
unlocks ttm_eu_fence_buffer_objects() would do:
kldload drm # ww_mutex implementation lives in drm.ko
sh build.sh # make SYSDIR=/usr/src/sys
sh run.sh # dmesg | grep DF5138
Stock kernel (run.log): first_lock=0 second_lock=0 <- Linux contract: -37 (-EALREADY) ticket->acquired after unlocks=1 <- correct value: 0 (desync) RESULT=REPRODUCED
Fix validation
fix.diff adds the missing -EALREADY short-circuit to __wwlock().
Applied inside the guest /usr/src, drm.ko rebuilt (fix_build.log), loaded,
proof re-run (fix_run.log):
first_lock=0 second_lock=-37
ticket->acquired after unlocks=0
RESULT=FIXED
See unload_hang.txt for the pre-existing stock kldunload drm hang hit
during validation (environment, unrelated).
Files
df5138_wwproof.c,Makefile,build.sh,run.shbuild.log(module build),run.log+run.2.log(stock runs),fix_run.log(patched run),fix_build.log(patched drm.ko build)fix.diff,env.txt,unload_hang.txt,VERDICT.md,manifest.json,verdict.json
DF-5138 β VERDICT
status: reproduced (mechanism, runtime-proven on the audit guest) reproduced: 1 (mechanism) Β· impact: none on this guest (ceiling: local DoS on GPU hardware)
The finding
ttm_eu_reserve_buffers() (sys/dev/drm/ttm/ttm_execbuf_util.c:93-178)
contains a duplicate-BO separation mechanism that is dead code on
DragonFly:
ret = __ttm_bo_reserve(bo, intr, (ticket == NULL), ticket); // :113
...
} else if (ret == -EALREADY && dups) { // :119
struct ttm_validate_buffer *safe = entry;
entry = list_prev_entry(entry, head);
list_del(&safe->head);
list_add(&safe->head, dups);
continue;
}
It depends on the Linux ww_mutex contract: locking a ww_mutex already held
by the SAME acquire context returns -EALREADY. DragonFly's ww_mutex
(sys/dev/drm/linux_wwmutex.c, DFly-original) never does:
ww_mutex_init()initializes the lockmgr withLK_CANRECURSE(linux_wwmutex.c:62);- lockmgr's same-thread exclusive relock silently succeeds (sys/kern/kern_lock.c:125-143: "If the caller already holds the lock exclusively then we silently obtain another count");
- so
__wwlock()returns 0 and executesww->ctx = ctx; ww->stamp = ctx->stamp; ++ctx->acquired;a second time (linux_wwmutex.c:117-120). - grep confirms: no
-EALREADYproducer exists anywhere in linux_wwmutex.c / ww_mutex.h.
Consequences, traced and (for the core mechanism) runtime-proven:
- Duplicate BOs are recursively double-locked. Both consumers can
deliver duplicates from unprivileged userspace: the radeon CS relocs
chunk looks up each handle independently (radeon_cs.c:103-104 β same
handle twice β two entries), and
amdgpu_bo_list_create()does not dedupe either (amdgpu_bo_list.c: lookup per info[i].bo_handle). On Linux the second entry goes todups; on DFly both stay inlistand the resv is locked twice. - Ticket accounting desync.
ww_mutex_unlock()(linux_wwmutex.c:188-202) decrementsctx->acquiredand NULLsww->ctxonly on the FIRST unlock of a resv; the second unlock of the recursive hold findsww->ctx == NULLand skips the decrement βctx->acquiredover-counts. Measured: acquired=1 after balanced lock/unlock (run.log). Effect:__wwlock()later takes the LK_NOWAIT+tsleep route (linux_wwmutex.c:110-114) where a plain blocking lock was correct β behavioral skew only (liveness analyzed sound). - Failure-path wedge (conditional). If the uh-oh path fires between
the two occurrences (e.g. -EDEADLK), the reverse-backoff unlocks the
first episode, the loop re-acquires, and lock counts re-balance
(traced); but if
reservation_object_reserve_shared()fails (krealloc -ENOMEM, linux_reservation.c:93-94) on the SECOND occurrence, the reverse-backoff releases the first episode and the second recursive episode is never unlocked (ttm_eu_backoff_reservation_reverseis exclusive of the current entry β list.h:187-189) β the BO's reservation lock is permanently held. This is the DF-2136 leak made reachable via a user-controlled duplicate list entry (on Linux the dup would have been moved todupsand never double-locked). Every later CS touching that BO blocks inww_mutex_lock_slow(killable when intr, unkillable for theintr=falsecallers, e.g. amdgpu_gem.c:177). - Fence side is refcount-safe (traced, killed as a corruption
candidate):
reservation_object_add_shared_fence/add_excl_fencetake their own refs (linux_reservation.c:109/151/244); a shared fence with the same context replaces in place (linux_reservation.c:120-127). Double fence installation on a duplicated resv is semantically odd but refcount-balanced, and the lockmgr count balances (two LK_RELEASE for two acquisitions).
A second consumer already expects the contract: modeset_lock()
(drm_modeset_lock.c:270-276) maps -EALREADY to "we already hold the lock..
this is fine" β dead code on DFly today for the same reason.
Runtime proof (guest, stock kernel)
df5138_wwproof.ko performs the exact ttm_eu_reserve_buffers sequence for
a duplicate BO (two ww_mutex_lock(&ww, &ctx), then the unlocks
ttm_eu_fence_buffer_objects() would perform β one per entry still in the
list). Stock run (run.log, run.2.log β 3 identical runs total):
DF5138-PROOF: first_lock=0 second_lock=0 (EALREADY would be -37) DF5138-PROOF: ticket->acquired after both locks = 2 (...) DF5138-PROOF: after unlocks ticket->acquired = 1 (correct value: 0; 1 proves the desync) DF5138-PROOF: resv still locked? 0 (...) DF5138-PROOF: RESULT=REPRODUCED (no -EALREADY; silent recursive double-lock; ticket accounting desync)
ww_mutex_is_locked()==0 also confirms the success path is lock-balanced
(the bug is NOT a straight memory-corruption primitive β the dangerous
manifestations are the failure-path wedges in 2/3 above, hardware-gated
behind a GPU CS ioctl this guest lacks).
Fix validation (performed in the same run)
fix.diff restores the contract in __wwlock():
if (ctx != NULL && ww->ctx == ctx)
return (-EALREADY);
Safety: ctx is private to the calling thread; observing ww->ctx == ctx
implies this thread holds the lock (own store visible to own load; no other
thread can store our ctx pointer). Applied inside the guest /usr/src,
drm.ko rebuilt (fix_build.log, 105 compile units, clean -Werror), installed,
guest rebooted (see unload_hang.txt β stock kldunload drm hang forced a
reboot; environment issue, unrelated), proof re-run (fix_run.log):
DF5138-PROOF: first_lock=0 second_lock=-37 (EALREADY would be -37) DF5138-PROOF: ticket->acquired after both locks = 1 (...) DF5138-PROOF: after unlocks ticket->acquired = 0 (correct value: 0; ...) DF5138-PROOF: resv still locked? 0 (...) DF5138-PROOF: RESULT=FIXED (-EALREADY returned; dup separated; accounting correct)
With the fix, ttm_eu_reserve_buffers' dups branch is live, the duplicate
is separated, and both the modeset -EALREADY handler and the ttm dups
handling behave as upstream intends. Baseline REPRODUCED / patched FIXED β
bad behavior gone.
Why severity is Low
- The mechanism is certain and runtime-proven, but the memory-safety-adjacent consequences (3) need (a) radeon/amdgpu GPU hardware (absent on the audit guest), (b) a duplicate handle in a CS list, and (c) an allocation failure at the right instant β narrow trigger, local DoS ceiling, overlapping DF-2136's leak family for the wedge symptom.
- No information disclosure, no corruption on the success path.
Exploit chain
(conditional DoS only: CS ioctl with duplicate BO handle + reserve_shared -EINTR/-ENOMEM timing β permanently-held resv lock β all later users of that BO block; no escalation path β the desync never produces attacker-controlled memory writes.)
Environment note
kldunload drm of the STOCK module hangs on this guest (D-state, holds the
kld lock; unload_hang.txt) β pre-existing, unrelated to the finding/fix.
Guest left clean via vm.sh reset with-src.
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
fixedfix.diff applied to guest /usr/src/sys/dev/drm/linux_wwmutex.c, drm.ko rebuilt (fix_build.log: 105 compile units, clean -Werror build, 20s), installed to /boot/kernel, guest rebooted, proof module re-run: second_lock=-37 (-EALREADY as on Linux), ticket->acquired=0 after unlocks, RESULT=FIXED (fix_run.log). Baseline (stock drm.ko) had second_lock=0 / acquired=1 / RESULT=REPRODUCED. git apply --check clean against the unmodified tree.
fix_run.log ('DF5138-PROOF: RESULT=FIXED (-EALREADY returned; dup separated; accounting correct)'); fix_build.log (full patched drm.ko build); run.log (baseline RESULT=REPRODUCED); unload_hang.txt (stock kldunload drm environment hang documented, unrelated to fix)
Confirmed kernel references
- sys/dev/drm/ttm/ttm_execbuf_util.c:113
- sys/dev/drm/ttm/ttm_execbuf_util.c:119-124
- sys/dev/drm/linux_wwmutex.c:62
- sys/dev/drm/linux_wwmutex.c:109-120
- sys/dev/drm/linux_wwmutex.c:188-202
- sys/kern/kern_lock.c:125-143
- sys/dev/drm/drm_modeset_lock.c:270-276
- sys/dev/drm/linux_reservation.c:69-99
- sys/dev/drm/radeon/radeon_cs.c:103-104
- sys/dev/drm/amd/amdgpu/amdgpu_bo_list.c:52-100
Detail
Exploit chain
conditional local DoS on GPU-equipped systems: CS ioctl listing the same GEM handle twice -> ttm_eu_reserve_buffers recursively double-locks the resv -> if reserve_shared fails on the second episode (memory pressure), reverse-backoff releases only the first -> BO reservation lock held forever -> every later CS/validation touching that BO blocks (unkillable for intr=false callers). No memory-write primitive, no escalation path.
Evidence (decisive lines)
run.log + run.2.log (stock: 'first_lock=0 second_lock=0 (EALREADY would be -37)', 'after unlocks ticket->acquired = 1 (correct value: 0...)', RESULT=REPRODUCED); fix_run.log (patched drm.ko: 'first_lock=0 second_lock=-37', 'ticket->acquired = 0', RESULT=FIXED); fix_build.log (105-unit drm.ko rebuild with fix.diff, clean); build.log (proof module build); VERDICT.md source trace: ttm_execbuf_util.c:113/119-124, linux_wwmutex.c:62/109-120/188-202, kern_lock.c:125-143, list.h:187-189, linux_reservation.c:93-94, drm_modeset_lock.c:270-276 (second consumer already handling -EALREADY = dead code today), radeon_cs.c:103-104 + amdgpu_bo_list.c (no dedupe of duplicate handles)
PoC changes
authored from scratch (no seed): KLD performing ttm_eu's exact duplicate-BO lock/unlock sequence against the live drm.ko ww_mutex implementation; module made fix-aware (second unlock only when second lock returned 0, mirroring the dups separation) so the same binary validates baseline and patched kernels; build needed the drm compat KCFLAGS include set (sys/dev/drm/drm/Makefile:121-126) and sys/conf.h for DEV_MODULE
Verified recommended fix
In __wwlock() (sys/dev/drm/linux_wwmutex.c) return -EALREADY when the lock is already held by the calling acquire context: 'if (ctx != NULL && ww->ctx == ctx) return (-EALREADY);' β restoring the Linux ww_mutex contract that ttm_execbuf_util.c:119-124 and drm_modeset_lock.c:270-276 already code against
Verdict
Mechanism runtime-reproduced on the audit guest with a KLD that performs ttm_eu_reserve_buffers' exact duplicate-BO sequence: ww_mutex_lock twice with the same ticket returned 0 the second time (Linux contract: -EALREADY=-37; no -EALREADY producer exists in sys/dev/drm/linux_wwmutex.c because ww_mutex_init sets LK_CANRECURSE at linux_wwmutex.c:62 and kern_lock.c:125-143 silently recurses same-thread exclusive relocks), making the dups branch at ttm_execbuf_util.c:119-124 dead code; after the two matching unlocks ticket->acquired stayed 1 instead of 0 (ww_mutex_unlock at linux_wwmutex.c:188-202 decrements only the first unlock of a recursive hold). Success path is lock-count balanced (ww_mutex_is_locked==0 after) β NOT a direct corruption primitive. Traced ceilings: (a) duplicate BO + reservation_object_reserve_shared failure (krealloc -ENOMEM, linux_reservation.c:93-94) leaves the second recursive episode permanently held (DF-2136-class wedge reachable through a user-controlled duplicate CS entry; later CS users of the BO block forever, unkillable for intr=false callers like amdgpu_gem.c:177); (b) acquired over-count forces the NOWAIT+tsleep route on later same-ticket locks. Duplicate handles are deliverable by unprivileged userspace on both consumers (radeon_cs.c:103-104 per-entry lookup; amdgpu_bo_list.c no dedupe). Full chain needs GPU hardware the audit guest lacks (0 AMD/ATI PCI devices), hence impact=none here and severity Low. fix.diff (restore -EALREADY in __wwlock) applied in guest /usr/src, drm.ko rebuilt clean (-Werror) and reloaded: proof printed second_lock=-37, acquired=0, RESULT=FIXED β bad behavior gone.
No comments yet.