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)
PoC verification
Evidence pack
findings/poc/DF-4864 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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 needsCHIP_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_pdeemits 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)
- 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.
- 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.
- 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, finalcc -r -o amdgpu.ko ... amdgpu_vm.olink 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) andmake cleanrun; guest returned viavm.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_testablefix.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/).
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_vm.c:855
- sys/dev/drm/amd/amdgpu/amdgpu_vm.c:861-876
- sys/dev/drm/amd/amdgpu/amdgpu_vm.c:878
- sys/dev/drm/amd/amdgpu/amdgpu_vm.h:293
- sys/dev/drm/amd/amdgpu/sdma_v4_0.c:1184-1200
- sys/dev/drm/amd/amdgpu/sdma_v4_0.c:1208-1222
- sys/dev/drm/amd/amdgpu/amdgpu_ib.c:61-81
- sys/dev/drm/amd/amdgpu/amdgpu_sa.c:151-173
- sys/dev/drm/amd/amdgpu/gmc_v9_0.c:941-949
- sys/dev/drm/amd/amdgpu/amdgpu_vm.c:3172
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).
No comments yet.