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

postclose teardown order reversed: amdgpu_vm_fini before amdgpu_ctx_mgr_fini (DF-port divergence) - VM page tables freed under still-live context jobs (device-side UAF / GPU loss)

Summary

amdgpu_driver_postclose_kms calls vm_fini BEFORE ctx_mgr_fini. Upstream Linux invariantly finishes contexts first (verified in v5.0, v5.2, v5.4) precisely so context scheduler entities and in-flight jobs quiesce before the VM is destroyed. The DF order frees every GPU page table, drops the PASID, and destroys the VM entity while a context job can still be queued or executing - the scheduler's dependency callback reads the torn-down VM and executing IBs walk freed page tables: device-side UAF -> GPU faults/hang/reset triggered by an unprivileged render client closing its fd with CS in flight. Aggravated by the DF scheduler port dropping the fence-wait from entity_fini, leaving the ordering as the only teardown barrier. Swap-the-calls fix (validated by apply-check + full amdgpu module rebuild).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4356 Β· 12 files
FileTypeDescriptionSize
trigger.c β€” 2.6 KB view raw
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 4.0 KB ↓ raw
build.sh β€” 82 B view raw
run.sh β€” 110 B view raw
run.log β€” 592 B view raw
env.txt β€” 414 B view raw
fix.diff β€” 608 B view raw
fix_apply_check.log β€” 146 B view raw
fix_build.log β€” 1.2 MB ↓ download
verdict.json β€” 4.7 KB view raw
manifest.json β€” 892 B view raw

DF-4356 β€” postclose teardown order: amdgpu_vm_fini before amdgpu_ctx_mgr_fini (DF-port divergence)

Build / Run / Expected

cc -O2 -o trigger trigger.c        # on a host with libdrm headers (drm/amdgpu_drm.h)
./trigger /dev/dri/renderD128      # as an UNPRIVILEGED user
# Expected on vulnerable host: within N iterations, dmesg shows
#   "amdgpu: ... VM fault" / ring timeouts / amdgpu_device_gpu_recover
# (GPU page tables freed while context jobs still executing),
# X/ Wayland session loses the GPU -> system-wide GPU DoS.
# Clean host: "done: 4096 close-with-live-context cycles", no dmesg noise.

This audit's QEMU guest has no AMD GPU (/dev/dri does not exist), so the runtime half could not be executed here; see VERDICT.md for the source-level proof and the guest evidence.

What the bug is

sys/dev/drm/amd/amdgpu/amdgpu_kms.c:1073-1074 (amdgpu_driver_postclose_kms):

    amdgpu_vm_fini(adev, &fpriv->vm);      /* DF order: VM FIRST */
    amdgpu_ctx_mgr_fini(&fpriv->ctx_mgr);  /* ...contexts after */

Upstream Linux β€” verified in v5.0, v5.2 and v5.4 β€” has the opposite order:

    amdgpu_ctx_mgr_fini(&fpriv->ctx_mgr);  /* flush contexts: entities wait */
    amdgpu_vm_fini(adev, &fpriv->vm);      /* only then free page tables */

That order is a lifetime invariant: amdgpu_ctx_mgr_fini β†’ amdgpu_ctx_mgr_entity_fini (amdgpu_ctx.c:569-591) is what quiesces every context scheduler entity (and, upstream, waits for the last scheduled job to complete). Only after contexts are quiesced is it safe for amdgpu_vm_fini (amdgpu_vm.c:3282-3361) to:

  • remove the PASID from adev->vm_manager.pasid_idr (:3300-3306),
  • drm_sched_entity_destroy(&vm->entity) (:3311),
  • free all vm->freed mappings (:3335-3343),
  • amdgpu_vm_free_pts() β€” free/unpin all GPU page tables (:3348-3351).

With the DF order, both the host scheduler thread and the GPU itself can still be working for a context while the VM is torn down underneath them:

  • host side: amdgpu_job_dependency (amdgpu_job.c:193-198) calls amdgpu_vmid_grab(vm = job->vm, ...) which reads vm->reserved_vmid[], vm->pasid and vm->entity.fence_context (amdgpu_ids.c:406-459) β€” a VM whose internals were already destroyed.
  • device side: an executing IB walks page tables that amdgpu_vm_free_pts has just freed β†’ device-side UAF of page-table memory β†’ GPU VM faults / ring hangs / device reset (system-wide GPU loss).

Aggravating factor (different file, noted for triage): the DF scheduler port's drm_sched_entity_fini (sys/dev/drm/scheduler/sched_entity.c:263-292) also dropped upstream's dma_fence_wait(entity->last_scheduled) β€” so nothing in the DF teardown path waits for in-flight jobs at all, widening this window on both orderings; the correct (upstream) ctx-then-vm order is the only guard left, and amdgpu_kms.c has it reversed.

Fix

fix.diff β€” restore the upstream order (2-line swap).

VERDICT.md
↓ download raw

DF-4356 VERDICT β€” postclose VM/CTX fini order swapped (DF-port divergence)

Status: INCONCLUSIVE (hardware-gated). Source-level: divergence certain. Runtime: NOT executed β€” this audit's QEMU guest has no AMD GPU.

1. The divergence is certain and DF-introduced

amdgpu_kms.c:1073-1074 runs amdgpu_vm_fini(adev, &fpriv->vm) BEFORE amdgpu_ctx_mgr_fini(&fpriv->ctx_mgr).

Upstream invariant (all verified from kernel.org sources fetched during this session):

  • v5.0 amdgpu_kms.c:1055-1056: amdgpu_ctx_mgr_fini(...) then amdgpu_vm_fini(...)
  • v5.2 amdgpu_kms.c:1083-1084: same order
  • v5.4 amdgpu_kms.c:1113-1114 (approx): same order

The DF tree's amdgpu is a ~v5.0-base port (single VCN instance, .ready instead of .sched.ready, no PSP TA/RAS query cases). v5.0 has ctx-first, so the swap was made in the DragonFly port, not inherited.

2. Why the order is a lifetime guarantee (source trace)

postclose path, attacker = any local user who can open a render node (or authenticate on the primary node β€” the AMDGPU_CTX/CS ioctls this needs are DRM_AUTH|DRM_RENDER_ALLOW, amdgpu_kms.c:1198,1206):

  1. User allocates contexts (AMDGPU_CTX_OP_ALLOC_CTX), submits CS jobs (AMDGPU_CS) on them β€” jobs are pushed to ctx->entities[][].
  2. User close()s the fd with jobs still queued/executing.
  3. amdgpu_driver_postclose_kms (amdgpu_kms.c:1040): * amdgpu_vm_fini FIRST β€”
    • pasid removed from pasid_idr (vm.c:3300-3306)
    • drm_sched_entity_destroy(&vm->entity) (vm.c:3311)
    • all vm->freed mappings freed (vm.c:3335-3343)
    • amdgpu_vm_free_pts frees every page-table BO (vm.c:3348-3351)
    • amdgpu_ctx_mgr_fini SECOND β€” only NOW are context entities fini'd (ctx.c:569-591 β†’ drm_sched_entity_fini per entity).
  4. In the window between 3's free_pts and the ctx entity teardown, the scheduler thread may still be running a context job: * amdgpu_job_dependency (job.c:173-204) dereferences job->vm and calls amdgpu_vmid_grab (ids.c:406-459) touching vm->reserved_vmid[vmhub] (:422), vm->pasid (:459), vm->entity.fence_context (:452) of the destroyed VM; * an already-scheduled IB executing on the GPU walks page tables that were freed at step 3 β†’ device-side UAF of GPU page tables.

Note on host-side memory safety: the struct amdgpu_vm itself is embedded in fpriv and only kfree'd at amdgpu_kms.c:1086 (after both finis), so the host-side hazard is reading stale/destroyed internals (idr removal, freed mappings) rather than freed slab memory; the primary memory-safety hazard is the device-side page-table UAF. Aggravator: DF's drm_sched_entity_fini (sched_entity.c:263-292) dropped upstream's dma_fence_wait(last_scheduled), so nothing waits for executing jobs at teardown β€” the ctx-then-vm ordering is the only remaining barrier and amdgpu_kms.c has it backwards.

3. Runtime attempt on this audit's guest (honest negative)

  • uname -a: DragonFly dfbsd 6.5-DEVELOPMENT ... X86_64_GENERIC (env.txt)
  • ls /dev/dri β†’ No such file or directory; pciconf -l shows no vendor 0x1002 device (no AMD GPU attached to the VM).
  • trigger.c compiled path therefore cannot run here; the run.log records the device-absent evidence gathered on the guest.

4. Fix

Two-line swap restoring the upstream order β€” fix.diff. Low risk: no code between the two calls uses either structure, so the swap cannot introduce a new dependency.

git apply --check verified against the guest's clean /usr/src copy (fix_apply_check.log). Full patched-kernel behavioral validation requires AMD hardware (page-table UAF is device-visible), which this guest cannot provide β†’ fix_status: not_testable.

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.diff restores the upstream (ctx-then-vm) order. patch --dry-run apply-check passed on the guest's pristine /usr/src (fix_apply_check.log, pristine md5 c38b5273...); with the fix applied the whole amdgpu module rebuilt cleanly (make -j6 in sys/dev/drm/amd/amdgpu -> amdgpu.ko, amdgpu_kms.o zero warnings, mod_done_0 - full log in fix_build.log). Behavioral validation (close-race with in-flight CS on real hardware) is not possible on this GPU-less guest, hence not_testable rather than fixed.

fix_apply_check.log (DF-4356_APPLY_CHECK_OK, pristine md5 c38b5273f3ad36ab64d5fc2fd782fdbb); fix_build.log (FULL 777-line module build: amdgpu_kms.o compiled zero-warnings, amdgpu.ko linked, mod_done_0)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (buildkernel compile-validation only; behavioral fix test needs AMD GPU)

Confirmed kernel references

Detail

Exploit chain

unprivileged render-node client: open /dev/dri/renderD128 -> AMDGPU_CTX alloc -> AMDGPU_CS submit (long-running IB) -> close(fd) racing scheduler -> postclose frees page tables under executing job -> GPU page-table UAF -> GPU VM fault/ring hang -> device reset (system-wide GPU loss). Cross-process VRAM disclosure via freed-PT reuse is theoretically open but unproven without hardware.

Evidence (decisive lines)

VERDICT.md sections 1-3 (upstream v5.0/v5.2/v5.4 citations; amdgpu_vm.c:3300-3351 teardown inventory; amdgpu_job.c:193-198 + amdgpu_ids.c:406-459 post-teardown derefs; sched_entity.c:263-292 missing wait); run.log + env.txt (guest hardware-absent proof); fix_apply_check.log; trigger.c (compile-ready race trigger); fix.diff + fix_build.log (patched kernel build)

PoC changes

n/a (no seed PoC existed; trigger.c authored fresh; runtime blocked by absent hardware, not by code defects)

Verified recommended fix

Swap amdgpu_ctx_mgr_fini() before amdgpu_vm_fini() in amdgpu_driver_postclose_kms to restore the upstream Linux teardown order.

Verdict

DragonFly-port teardown-order divergence, source-certain: amdgpu_kms.c:1073-1074 runs amdgpu_vm_fini BEFORE amdgpu_ctx_mgr_fini, while upstream Linux v5.0/v5.2/v5.4 (all fetched and verified this session) invariantly finish contexts first so their scheduler entities and in-flight jobs quiesce before the VM page tables are freed. With the DF order, amdgpu_vm_fini frees all GPU page tables (amdgpu_vm.c:3348 amdgpu_vm_free_pts), drops the PASID and destroys vm->entity while context jobs can still be executing or queued: the scheduler thread's amdgpu_job_dependency (amdgpu_job.c:193-198) calls amdgpu_vmid_grab on the torn-down VM (amdgpu_ids.c:406-459) and executing IBs walk freed page tables -> device-side UAF -> GPU faults/hang/reset (system-wide GPU DoS); the DF scheduler port additionally dropped upstream's dma_fence_wait(last_scheduled) in drm_sched_entity_fini (sched_entity.c:263-292), removing any other barrier. Runtime reproduction is hardware-gated: the audit QEMU guest has no AMD GPU, no /dev/dri and no amdgpu UAPI header (all verified, run.log/env.txt), so status is inconclusive, consistent with prior amdgpu PoCs DF-1467/DF-1595. Fix is the two-line swap restoring the upstream order (fix.diff, apply-check OK on guest /usr/src, compile-validated by patched buildkernel).