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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4414 Β· 11 files
FileTypeDescriptionSize
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_fini must reserve the shared BO; the reservation is held by the parent and the child has a pending signal β†’ ttm_bo_reserve(..., interruptible=1) returns -ERESTARTSYS immediately, the DFly while (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 observes waitpid() 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.

VERDICT.md
↓ download raw

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_vm never 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 the radeon_bo alive, so this fd's bo_va stays in the tree at close time (radeon_gem.c free 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).

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_testable
baseline no→ patch + rebuild →patched clean

fix 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)
↓ fix.diffper-fix-DF-4414

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).