radeon_vm_fini() DragonFly rewrite spins forever when radeon_bo_reserve() fails β unkillable kernel livelock (upstream single-pass merely leaks)
Summary
The DFly branch replaced upstream's single-pass postorder walk with 'while (vm->va.rb_node) { reserve(); if (!r) remove; }' because the interval-tree mock is a circular list. radeon_bo_reserve(bo,false) is INTERRUPTIBLE: a signalled exiting thread hitting a BO whose reservation another process holds mid-CS gets -ERESTARTSYS immediately, the root never changes, and the loop spins forever - unkillable thread, never-released fd, leaked vm, pegged CPU. DFly-local divergence. Uninterruptible-reserve + bail fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4414 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger_fini.c | β | 4.1 KB | view raw | |
| radeon_abi.h | β | 2.4 KB | view raw | |
| build.sh | β | 103 B | view raw | |
| run.sh | β | 98 B | view raw | |
| build.log | β | 489 B | view raw | |
| run.log | β | 489 B | view raw | |
| VERDICT.md | β | 3.1 KB | β raw | |
| README.md | β | 1.3 KB | β raw | |
| fix.diff | β | 1.2 KB | view raw | |
| verdict.json | β | 2.1 KB | view raw | |
| env.txt | β | 585 B | view raw |
DF-4414 β radeon_vm_fini() DragonFly livelock: while (vm->va.rb_node) spins forever when radeon_bo_reserve() fails
Build
cc -O2 -Wall -o /tmp/trigger_fini DF-4414/trigger_fini.c # radeon_abi.h must sit next to the .c
Run
/tmp/trigger_fini
Requires a cayman-or-newer radeon KMS device and two cooperating processes sharing a BO (GEM flink).
Expected result on capable hardware
- Parent: maps a flink-shared BO into its vm, keeps submitting CS so the
BO's TTM reservation is (repeatedly) held via
ttm_eu_reserve_buffers. - Children: GEM_OPEN the shared BO, map it into their own vm, then exit
with SIGKILL racing the close. A child's
postclose β radeon_vm_finimust reserve the shared BO; the reservation is held by the parent and the child has a pending signal βttm_bo_reserve(..., interruptible=1)returns-ERESTARTSYSimmediately, the DFlywhile (vm->va.rb_node)loop makes no progress and never exits β the child's thread spins forever in kernel mode (unkillable), the drm fd never closes, and the vm is leaked. Parent observeswaitpid()never returning.
Observed on THIS audit guest
No radeon GPU (/dev/dri absent). Trigger builds and runs, exits at
device open (run.log). Mechanism proven by trace in VERDICT.md β the
divergence from upstream is DFly-specific.
DF-4414 β VERDICT
status: not_reproduced (hardware absent) β DFly-specific divergence from upstream; mechanism certain by trace
Root cause (certain)
DFly replaced upstream's rbtree_postorder_for_each_entry_safe() walk in
radeon_vm_fini() with a retry-forever loop over the interval tree root
(introduced when the DFly interval-tree mock β a circular list, see
sys/dev/drm/linux_interval_tree.c β made the postorder RB iterator
unusable):
1250: while (vm->va.rb_node) {
1251: bo_va = container_of((void *)vm->va.rb_node, struct radeon_bo_va, it);
1253: r = radeon_bo_reserve(bo_va->bo, false);
1254: if (!r) {
1255: interval_tree_remove(&bo_va->it, &vm->va);
... kfree(bo_va);
1260: }
1261: }
If radeon_bo_reserve() fails, the tree root is unchanged β the loop
repeats the identical failing operation forever.
radeon_bo_reserve(bo, false) is
ttm_bo_reserve(&bo->tbo, /*interruptible=*/!false, /*no_wait=*/false, NULL)
(radeon_object.h:64-68): when the BO's reservation object is held by
another thread and the current thread has a pending signal it returns
-ERESTARTSYS immediately, without blocking. The closing thread is
exiting precisely because it was signalled β tight kernel-mode spin loop,
no blocking, no exit condition:
- the exiting thread never returns to user mode β unkillable,
- the drm file is never released, the
radeon_vmnever freed (leak), - one CPU pegged at 100% in kernel space.
Upstream Linux performs a single pass
(rbtree_postorder_for_each_entry_safe) and merely leaks the bo_va on
reserve failure (dev_err "still active bo inside vm" at radeon_vm.c:1237
mirrors that). DFly's rewrite turned a bounded leak into an unbounded
livelock. This is a genuine DFly-local defect, not an inherited one.
Reachability
Requires vm->va to be non-empty at radeon_vm_fini() and a
reserve failure:
- Normal exits empty the vm first (GEM object teardown calls
radeon_vm_bo_rmv), BUT a BO shared with another process via GEM flink / dma-buf keeps theradeon_boalive, so this fd'sbo_vastays in the tree at close time (radeon_gem.cfree path only rmv's when the object dies). - The other process keeps the shared BO's reservation held across CS
parsing (
ttm_eu_reserve_buffersβradeon_bo_list_validate, radeon_cs.c:184-190) while the victim exits under a signal.
All steps are ordinary unprivileged DRM operations.
Why not reproduced here
No radeon GPU on the audit guest (std VGA only). Trigger builds and runs,
exits at open("/dev/dri/card0") (run.log). Severity Medium (local DoS:
permanently wedged kernel thread + unkillable fd + vm leak; repeatable).
Recommended fix
Make progress mandatory: use an uninterruptible reserve for teardown and
bail out (leak, like upstream) rather than spinning if it still fails.
See fix.diff.
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
not_testablefix validation impossible on this guest (no radeon GPU). fix.diff switches the reserve to no_intr=true and adds a bail-out branch.
fix.diff (radeon_bo_reserve(bo, true) + break on failure)
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (livelock mechanism + upstream divergence); run.log (device-absent); fix.diff (uninterruptible reserve + bail-out)
PoC changes
kernel uapi include replaced by self-contained radeon_abi.h
Verified recommended fix
Use uninterruptible reserve for teardown and break (bounded leak, matching upstream) instead of spinning
Verdict
DFly-specific livelock certain by trace: radeon_vm_fini():1250-1261 loops 'while (vm->va.rb_node)' and only removes the node when radeon_bo_reserve() succeeds; reserve is interruptible (radeon_object.h:64-68) so an exiting signalled thread hitting a BO whose reservation another process holds mid-CS gets -ERESTARTSYS immediately and spins in kernel mode forever (unkillable thread, leaked vm+fd). Upstream's single-pass postorder walk only leaks. The audit guest has no radeon GPU so the hang cannot be triggered at runtime; trigger compiles and runs, exits at open()=ENOENT (run.log). Analysis-classified impact: local DoS (Medium).
No comments yet.