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

amdgpu_vm_clear_bo() 64-byte IB overrun: the ATS root-PD clear emits two 10-dword set_pte_pde packets + SDMA pad (24 dwords) into a 16-dword (64-byte) SA sub-allocation β€” deterministic 32-byte kernel heap OOB write per GPU-VM create on every AMD Raven APU

Summary

The unbounded pad/emit contract defined in amdgpu_ring.c:107-111 (while length_dw&align_mask ptr[length_dw++]=nop, no bound) is fed an undersized IB: the VM clear-IB is 64 bytes but on CHIP_RAVEN (the only pte_support_ats ASIC) the root-PD clear takes BOTH emit branches - 2x10 dwords + 4 pad = 24 dwords into 16, a deterministic 32-byte overwrite past the SA sub-allocation on every unpriv GPU-VM create. The WARN guard compares dwords to bytes (unit-confused, can never trip). Overrun corrupts neighboring jobs' command streams cross-context, or with SA-pool grooming past the 1MB pool mapping. VERBATIM-code harness run on guest: 32 canary bytes destroyed for both Raven revs, non-ATS case fits exactly (trigger specificity); fix (128B IB + corrected WARN) compile-validated on full -Werror amdgpu.ko. No AMD ASIC on guest for runtime.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4864 Β· 13 files
FileTypeDescriptionSize
df4864_trigger.c β€” 8.3 KB view raw
build.sh β€” 149 B view raw
run.sh β€” 146 B view raw
build.log β€” 25 B view raw
run.log β€” 2.3 KB view raw
run.2.log β€” 2.3 KB view raw
env.txt β€” 464 B view raw
fix.diff β€” 850 B view raw
fix_build.log β€” 105.2 KB view raw
README.md β€” 4.4 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
manifest.json β€” 1.8 KB view raw
verdict.json β€” 4.3 KB view raw

DF-4864 β€” amdgpu_vm_clear_bo() 64-byte IB overrun (Raven root-PD clear)

What this is

amdgpu_vm_clear_bo() (sys/dev/drm/amd/amdgpu/amdgpu_vm.c) sizes its clear-IB at 64 bytes (16 dwords):

amdgpu_vm.c:855   r = amdgpu_job_alloc_with_ib(adev, 64, &job);

On CHIP_RAVEN β€” the only ASIC with pte_support_ats (amdgpu_vm.c:3172) β€” the root page-directory clear takes both emit branches (amdgpu_vm.c:861-874):

if (ats_entries) amdgpu_vm_set_pte_pde(...);   /* 10 dwords */
if (entries)     amdgpu_vm_set_pte_pde(...);   /* 10 dwords */

amdgpu_vm_set_pte_pde is a macro (amdgpu_vm.h:293) into sdma_v4_0_vm_set_pte_pde() (sdma_v4_0.c:1184-1200) which emits exactly 10 dwords per call, unbounded (ib->ptr[ib->length_dw++]). The following amdgpu_ring_pad_ib() (sdma_v4_0.c:1208-1222) appends up to 7 more dwords of alignment NOPs, also unbounded.

Result: 24 dwords are written into a 16-dword (64-byte) kernel SA sub-allocation β€” a deterministic 32-byte kernel heap OOB write, executed on every GPU VM creation (DRM_IOCTL_AGP.../AMDGPU VM ioctls β€” unprivileged with /dev/drm access) on Raven APUs.

Trigger arithmetic (Raven rev0/1: gmc_v9_0.c:941 β†’ vm 256TB, block 9, 3 levels): root_level = PDB2; root PD = round_up(2^36, 2^27) >> 27 = 512 entries (amdgpu_vm_num_entries, amdgpu_vm.c:244-248); ats_entries = 2^47 >> (27 + 12) = 256; entries = 512 - 256 = 256 β†’ both branches. Raven revβ‰₯2 (gmc_v9_0.c:945, 128TB+512GB): 257-entry root PD, ats=256/entries=1 β†’ both branches as well.

The non-ATS / non-root case emits a single 10-dword packet + 6 pad dwords = 16 dwords = exactly 64 bytes (no overrun) β€” the sizing was correct only for that case, and the guard WARN_ON(job->ibs[0].length_dw > 64) (amdgpu_vm.c:878) compares a dword count against the byte size, so it can never trip before the overrun already happened.

Where the overrun lands

The IB comes from the 1MB SA sub-allocator pool (AMDGPU_IB_POOL_SIZE(16) x 64KB, amdgpu_ib.c:287) via amdgpu_sa_bo_new(..., 64, 256) (amdgpu_ib.c:67-68; first-fit packing, amdgpu_sa.c:151-173). The 32 overrun bytes land in whichever kernel memory follows the 64-byte sub-allocation:

  • normally the slack before the next SA sub-allocation (or the next job's IB β€” cross-context GPU command corruption), or
  • when the pool is packed to its end (attacker-groomable by spraying CS IBs β€” GFX CS IBs are user-sized SA allocations, amdgpu_cs.c:1033), past the end of the 1MB SA buffer's kernel mapping (ttm/vmap) β†’ kernel heap OOB write into adjacent kernel address space.

Overrun content is deterministic: dword 16 = value_hi of the second GENERATE_PTE_PDE packet, 17 = incr (0), 18 = 0, 19 = count - 1 (entries-1), then 4 SDMA NOP headers (0x00000000), then zeros β€” i.e. partially influenceable through VRAM placement of the root PD (value/count).

Files

  • df4864_trigger.c β€” userspace replica of the exact kernel emit/pad sequence (code copied verbatim from sdma_v4_0.c) against a canaried 64-byte buffer, plus a hardware-reachability probe.
  • build.sh / run.sh β€” exact build/run.
  • build.log / run.log / run.2.log β€” full untrimmed guest output.
  • fix.diff β€” one-hunk fix (IB sized 128 bytes; WARN bound corrected to 32 dw).
  • fix_build.log β€” full amdgpu.ko module build with fix.diff applied (compiles clean under -Werror; module links).
  • env.txt β€” guest environment.

Build / run (inside the DragonFly guest)

sh build.sh        # cc -O2 -Wall -o df4864_trigger df4864_trigger.c
sh run.sh          # runs the three cases + hw probe

Expected output

[Raven-root-512] ... *** OOB WRITE: 8 dwords (32 bytes) past the 64-byte SA sub-allocation ***
[Raven2-root-257] ... *** OOB WRITE: 8 dwords (32 bytes) past the 64-byte SA sub-allocation ***
[non-ATS-single] ... fits exactly (no overrun) β€” this is the non-ATS / non-root case

Runtime status on the audit guest

The audit QEMU guest has no AMD GPU (only a Bochs 0x1234:1111 std VGA; no /dev/dri, no /dev/pci access as unpriv user, no drm modules) β€” the kernel path cannot execute there. The kernel-side overrun is therefore proven from source arithmetic (deterministic, no Heisenberg conditions) and demonstrated byte-for-byte by the replica; it is not runtime-reproduced on this guest. On real Raven hardware (Ryzen 2200G/2400G and Raven mobile) the path runs on every VM create.

VERDICT.md
↓ download raw

DF-4864 VERDICT β€” amdgpu_vm_clear_bo() 64-byte IB overrun

Status: not_reproduced (runtime) β€” hardware-gated kernel path; the bug itself is code-certain and its write pattern is demonstrated byte-exactly by the harness.

Reproduced?

  • Kernel-side: no β€” on this guest only because the hardware is absent. The QEMU audit guest has a single Bochs std-VGA (chip 0x11111234, no AMD 0x1002 device), no /dev/dri or /dev/drm nodes, and no drm/amdgpu modules loaded (env.txt). amdgpu_vm_clear_bo() is reachable only after amdgpu(4) attaches to a real AMD ASIC, and the dual-emit branch needs CHIP_RAVEN (amdgpu_vm.c:3172: pte_support_ats = (adev->asic_type == CHIP_RAVEN)).
  • Source-side: yes, deterministic. The overrun follows from unambiguous constants:
  • allocation: amdgpu_job_alloc_with_ib(adev, 64, &job) β€” amdgpu_vm.c:855 β†’ amdgpu_ib_get(..., 64, ...) (amdgpu_ib.c:61-81) β†’ SA sub-allocation of 64 bytes = 16 dwords.
  • emit: two calls through amdgpu_vm_set_pte_pde (macro, amdgpu_vm.h:293) β†’ sdma_v4_0_vm_set_pte_pde emits 10 dwords each (sdma_v4_0.c:1190-1200: header, pe_lo, pe_hi, mask_lo, mask_hi, value_lo, value_hi, incr, 0, count-1) β€” 20 dwords total.
  • pad: sdma_v4_0_ring_pad_ib β€” (8 - (20 & 7)) % 8 = 4 more dwords (sdma_v4_0.c:1214) β€” 24 dwords total = 96 bytes into 64 bytes.
  • precondition (both branches): raven rev0/1 root PD = 512 entries with ats_entries = 256 / entries = 256 (gmc_v9_0.c:941 β†’ vm 256TB/block 9/3 levels; amdgpu_vm_num_entries: round_up(2^36,2^27)>>27 = 512; AMDGPU_GMC_HOLE_START 2^47 >> (27+12) = 256); raven revβ‰₯2 root PD = 257 entries with ats 256 / entries 1 (gmc_v9_0.c:945). Verified in-guest that host and guest trees agree (amdgpu_vm.c:855 both).
  • Harness-side: yes. df4864_trigger (verbatim emit/pad code against a canaried 64-byte buffer) shows 10 β†’ 20 β†’ 24 dwords and 32 destroyed canary bytes for both Raven cases, and the exactly-fitting 16-dword result for the non-ATS single-packet case (run.log). This is a replica, not the kernel executing β€” hence not_reproduced rather than reproduced.

Why it is exploitable in the real world (impact ceiling)

  1. Unprivileged user opens /dev/drm* on a Raven APU and issues the VM-create ioctl family (every GEM VM init β†’ amdgpu_vm_init β†’ amdgpu_vm_clear_bo on the root PD, amdgpu_vm.c:3098-3101). No privilege, no races β€” the overrun happens on the very first clear, deterministically.
  2. The 64-byte IB is a sub-allocation of the 1MB SA pool (AMDGPU_IB_POOL_SIZE(16)*64KB, amdgpu_ib.c:286-289). Overrun bytes corrupt the neighbouring SA hole/next IB, or β€” when the pool is packed to its end, groomable by spraying user-sized GFX CS IBs (amdgpu_cs.c:1033-1035) β€” write 32 bytes past the pool's kernel (ttm/vmap) mapping into adjacent kernel address space.
  3. Written content: second packet's value_hi, incr(0), 0, count-1, four NOP headers (0), zeros β€” partially controllable (VRAM placement of the root PD influences value/count fields). Not a free-form write, but a repeatable 32-byte kernel heap overwrite at attacker-influenced offsets β€” a genuine kernel memory-corruption primitive (GPU command corruption of other contexts even in the benign placement case).

Not pushed further (no uid0 chain) because the required hardware is absent: uid0 escalation would need a Raven APU to groom the SA pool end and shape the adjacent mapping β€” out of reach on this QEMU/Bochs guest.

Fix validation

  • fix.diff (IB sized 128 bytes = 32 dw β‰₯ 24 + slack; WARN bound corrected from 64-byte/16-dword confusion to 32 dw) applied cleanly to the guest's /usr/src (patch -p1, hunks at 852/882), and the full amdgpu.ko module rebuilt without warnings under -Werror (fix_build.log, final cc -r -o amdgpu.ko ... amdgpu_vm.o link line).
  • Runtime baseline/patched comparison impossible on this guest (no AMD GPU), so fix_status = not_testable; the fix is compile-validated and the size math (128 β‰₯ 24 dwords used) is provable from the same source constants.
  • Guest source tree reverted (patch -R, alloc back to 64 at :855) and make clean run; guest returned via vm.sh reset with-src.

Bottom line

Deterministic 32-byte kernel heap OOB write per GPU-VM create on AMD Raven APUs (every revision), from an unprivileged DRM client. Fixed by sizing the clear-IB for two PTE/PDE packets plus padding. Not runtime-reproducible on this audit guest for absence of AMD hardware; code-certain from source.

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 compiles clean against the guest's /usr/src (full amdgpu.ko module build under -Werror, fix_build.log); runtime baseline-vs-patched comparison impossible on this guest (no AMD GPU). 128 bytes covers the proven worst case (20 emit + 7 pad = 27 dwords).

findings/poc/DF-4864/fix_build.log (patch applied: hunks 852/882; final 'cc -r -o amdgpu.ko ... amdgpu_vm.o' link); fix.diff (git-apply-clean against sys/).
↓ fix.diffper-fix-DF-4864

Confirmed kernel references

Detail

Exploit chain

On real Raven hardware: unpriv /dev/drm client -> VM-create ioctl -> amdgpu_vm_init -> amdgpu_vm_clear_bo(root PD) -> 24 dwords written into 16-dword SA sub-allocation -> 32-byte overwrite of neighbouring SA data (cross-context GPU command corruption), or past the SA pool's kernel mapping when the pool is groomed full (spray GFX CS IBs, amdgpu_cs.c:1033) -> kernel heap corruption with semi-controlled content (value_hi/count of PTE packet + NOPs). No uid0 chain developed: required ASIC absent from the audit guest.

Evidence (decisive lines)

run.log lines '[Raven-root-512] *** OOB WRITE: 8 dwords (32 bytes) ...' and '[non-ATS-single] fits exactly'; VERDICT.md arithmetic section; fix_build.log final amdgpu.ko link line (patched module compiles -Werror clean); env.txt (guest has no AMD GPU).

PoC changes

No seed PoC existed (pass-2 discovery); harness authored from scratch with emit/pad code copied verbatim from sdma_v4_0.c.

Verified recommended fix

Size the clear-IB for the worst case (two set_pte_pde packets + SDMA pad): amdgpu_job_alloc_with_ib(adev, 128, &job) and fix the dword/byte confusion in the WARN (length_dw > 32).

Verdict

Deterministic 32-byte kernel heap OOB write per GPU-VM create on AMD Raven APUs (all revisions): amdgpu_vm_clear_bo() allocates a 64-byte/16-dword SA sub-allocation (amdgpu_vm.c:855) but the ATS root-PD clear emits two 10-dword set_pte_pde packets (amdgpu_vm.c:867-874 -> sdma_v4_0.c:1190-1200) plus 4 SDMA pad dwords = 24 dwords into 16. Trigger is CHIP_RAVEN-only (pte_support_ats, amdgpu_vm.c:3172): rev0/1 root PD = 512 entries split 256/256 (gmc_v9_0.c:941), rev>=2 = 257 entries split 256/1 (gmc_v9_0.c:945). The kernel-side path cannot run on the audit guest (QEMU Bochs 0x1234:1111 VGA, no AMD device, no /dev/dri, no drm modules) so it is not runtime-reproduced; the arithmetic is proven from unambiguous source constants and the byte-exact write pattern (10->20->24 dwords, 32 canary bytes destroyed; non-ATS case fits exactly at 16) is demonstrated by the verbatim-code harness in run.log. The guard WARN_ON(length_dw > 64) at amdgpu_vm.c:878 compares dwords to the byte size and never trips. Overrun lands in the adjacent SA hole / neighbouring IB, or past the 1MB SA pool mapping when pool-packed (groomable via user-sized GFX CS IBs).