amdgpu_gem: GEM_VA MAP/REPLACE commits page tables for the entire attacker-chosen range before amdgpu_vm_bo_map() validates the mapping β every failed map strands ~(map_size>>18) bytes of page-table BOs until fd close; unprivileged render-node GTT/kernel exhaustion (cross-process GPU wedge)
Summary
Validate-after-allocate ordering: alloc_pts runs on the raw user args and only afterwards does bo_map bounds-check offset_in_bo/map_size against the BO - a deliberately rejected mapping leaves all page tables for the whole range in the per-fd VM until fd close. A 4KiB BO plus one doomed ioctl pins ~512MiB-1GiB of page tables; repeating with fresh render-node fds exhausts GTT so amdgpu allocations fail for every GPU process. Legitimate PT allocation is bounded by bo_size; this ordering bug removes that bound. No auth/master needed (DRM_AUTH|RENDER_ALLOW). PoC builds clean on guest (no AMD GPU - untested/honest). Pre-validate-before-alloc_pts fix (git-apply-verified), which also pre-fixes the DF-1719 wrap for this path.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4576 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| gem_va_pt_exhaust.c | β | 6.5 KB | view raw | |
| build.sh | β | 108 B | view raw | |
| run.sh | β | 151 B | view raw | |
| build.log | β | 75 B | view raw | |
| run.log | β | 301 B | view raw | |
| env.txt | β | 393 B | view raw | |
| VERDICT.md | β | 6.5 KB | β raw | |
| README.md | β | 2.2 KB | β raw | |
| fix.diff | β | 1.2 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-4576 β amdgpu GEM_VA alloc_pts-before-validate: page-table memory exhaustion
What it is
amdgpu_gem_va_ioctl() (MAP and REPLACE) allocates VM page tables for the
whole attacker-supplied [va_address, va_address+map_size) range via
amdgpu_vm_alloc_pts() before amdgpu_vm_bo_map() validates
offset_in_bo/map_size against the BO. When the validation rejects the
mapping (which the attacker deliberately provokes), the freshly-allocated
page-table BOs are never freed β they stay owned by the per-fd VM until the
DRM fd is closed. One 4 KiB BO + one failed ioctl pins ~(span >> 18) bytes of
GTT/VRAM (128 TiB span β ~512 MiB). Repeat with fresh render-node fds β
GTT/kernel-memory exhaustion β GPU unusable for every process (local,
cross-process DoS by an unprivileged render-node client).
Files
gem_va_pt_exhaust.cβ self-contained PoC (raw ioctls, no libdrm needed)build.sh/run.shβ exact commandsbuild.log/run.log/env.txtβ guest evidenceVERDICT.mdβ full analysis narrative + line-level tracefix.diffβ validation-before-allocation patch (NOT applied to sys/)manifest.json/verdict.json
Build
cc -O2 -Wall -o gem_va_pt_exhaust gem_va_pt_exhaust.c
(verified to build clean on the DragonFly 6.5-DEVELOPMENT audit guest)
Run
./gem_va_pt_exhaust [iterations] [span_bytes_hex] # defaults: 64, 0x800000000000 (128 TiB)
Expected output
On real AMD hardware (amdgpu render node present):
- every iteration prints MAP rejected (Invalid argument) β page tables for
128.0 TiB of VA now pinned by fd N β the EINVAL is the rejection of the
mapping; the leak is the already-committed page tables;
- GTT usage grows ~512 MiB per iteration
(cat /sys/kernel/debug/dri/<n>/amdgpu_mm_info);
- after enough iterations: amdgpu allocation failures in dmesg, GPU clients
fail for all users.
On a machine without an AMD GPU (this audit's QEMU guest: virtio VGA only,
/dev/dri absent, amdgpu not even in the guest kernel build):
iter 0: cannot open any DRM node (No such file or directory) -> no AMD GPU on this machine; PoC needs real amdgpu hardware.
exit code 2 β which is what run.log records.
DF-4576 VERDICT β amdgpu GEM_VA alloc_pts-before-validate page-table exhaustion
Status: untested (attempted on the audit guest; target hardware absent β see "Verification attempts"). Impact if hardware present: dos (local, cross-process GPU/GTT memory exhaustion by an unprivileged render-node client). Confidence in the code path: high β pure code trace, every hop cited.
Root cause (line-level trace)
-
amdgpu_gem.c:637-646(AMDGPU_VA_OP_MAP) andamdgpu_gem.c:657-666(AMDGPU_VA_OP_REPLACE): the ioctl callsamdgpu_vm_alloc_pts(adev, bo_va->base.vm, args->va_address, args->map_size)with the raw, still-unvalidated user arguments, and only afterwards callsamdgpu_vm_bo_map(... args->offset_in_bo, args->map_size ...). -
amdgpu_vm.c:945-971(amdgpu_vm_alloc_pts): the only validation is GPU-page alignment ofsaddr/sizeandeaddr >= max_pfn. For anything inside the VA limit it then walks the whole range (for_each_amdgpu_vm_pt_leaf, amdgpu_vm.c:973) and allocates a BO per missing page table (amdgpu_vm.c:995) pluskvmalloc_arrayentry arrays (amdgpu_vm.c:981). Nothing in the ioctl frees any of this. -
amdgpu_vm.c:2503-2512(amdgpu_vm_bo_map): the actual mapping validation βsize == 0, page alignment,saddr >= eaddrwrap, andoffset + size > amdgpu_bo_size(bo)β runs after the page tables are committed. An attacker forces this check to fail by passing a huge, page-alignedoffset_in_bo(e.g.0x1000000000000000), so the mapping is rejected with-EINVAL. -
amdgpu_gem.c:675-680(error path):error_backoff:merely releases the reservations; the page tables allocated in step 2 remain in the VM's page-table tree (vm->root) until VM teardown at fd close. Page tables are not freed on unmap in this driver generation, and here there is not even a mapping to unmap β the allocation is pure waste.
Damage arithmetic
With 4 KiB page tables of 512 PTEs mapping 2 MiB pages, one PTB covers 1 GiB
of VA. A failed MAP spanning S bytes of VA pins β S / 2^30 Γ 4 KiB =
S / 2^18 bytes:
- 128 TiB span β ~512 MiB per ioctl
- 256 TiB (full 48-bit VM) β ~1 GiB per ioctl
Each iteration needs only a 4 KiB GTT BO and one doomed ioctl. Opening a
new render-node fd creates a fresh VM (amdgpu_kms.c: amdgpu_driver_open),
so the loop open β GEM_CREATE(4K) β GEM_VA(MAP, doomed) Γ N pins N Γ ~1 GiB
until GTT is full. Consequences: amdgpu TTM allocations start failing for
every process using the GPU (cross-process DoS), kernel kvmalloc memory
is consumed by PD entry arrays, and the per-ioctl cost includes
amdgpu_vm_clear_bo of every new PT (long stalls). GTT is not freed until the
attacker closes the fds; the wedge persists while they are open. No
MAP_SIZE-vs-GTT accounting exists anywhere on this path.
Notably this is an amplification primitive: the legitimate way to force
page-table allocation (a real mapping) is bounded by offset+size <=
amdgpu_bo_size(bo), i.e. by how much GTT the attacker already paid for.
The bug removes that bound β 4 KiB of BO buys ~1 GiB of page tables.
Threat model / reachability
- Ioctls are registered
DRM_AUTH|DRM_RENDER_ALLOW(amdgpu_kms.c:1197-1213) β reachable by any unprivileged user with render-node access (/dev/dri/renderD128, conventionally 0666 or groupvideo). - No GPU command submission, no special privileges needed β just the two ioctls.
Distinction from known findings
- DF-1719 is the wrap of
offset+sizeinsideamdgpu_vm_bo_map(GPU page table corruption). This finding is the ordering bug in the ioctl β the leak happens even with non-wrapping, honest-to-bounds-check failure, and nothing is corrupted; the resource is simply never reclaimed. - DF-4522 (vm fini leak of mappings) is a different lifetime bug at teardown; the leak here persists during the fd's whole lifetime regardless of fini.
Verification attempts (why "untested")
- PoC authored self-contained (raw Linux-encoding ioctls, no libdrm), fixed
a comment-termination compile error, built clean on the DragonFly
6.5-DEVELOPMENT guest (
build.log:BUILD_RC=0). - Executed as the unprivileged user (
run.log):iter 0: cannot open any DRM node (No such file or directory), exit 2. - Guest environment (
env.txt): QEMU virtio VGA only (chip 0x1111:1234),/dev/dridoes not exist,amdgpudoes not appear in the guest'ssys/conf/files(driver not in this kernel build), no drm kld, no drm/ amdgpu lines in dmesg. The audit guest cannot attach an AMD GPU, so the finding cannot be executed to reproduction here β it requires real AMD hardware (or an AMD-GPU CI runner). Status recorded asuntested(device absent), impactnoneon this guest; expected impact on target hardware:dos.
The path itself is live in the shipped tree: the code under audit is
unconditional in amdgpu_gem.c and its call graph
(amdgpu_gem_va_ioctl β amdgpu_vm_alloc_pts β amdgpu_bo_create) has no
hardware-dependent guards beyond device attach.
Fix validation
fix.diff adds the bo_map-equivalent validation (alignment, size, wrap of
both va_address+map_size and offset_in_bo+map_size, and the
offset+size <= amdgpu_bo_size(bo) bound) to the ioctl before
amdgpu_vm_alloc_pts(), so a mapping that would be rejected can no longer
allocate page tables. The wrap guards additionally pre-fix the DF-1719 wrap
for the MAP/REPLACE ioctl path. Fix validation (fix_status) is
not_testable for the same reason as above: the guest cannot attach amdgpu
hardware, so baseline-vs-patched comparison cannot be executed. The patch is
a pure argument pre-check inserted before the operation switch; the cited
identifiers (AMDGPU_GPU_PAGE_MASK via amdgpu_gart.h, amdgpu_bo_size() via
amdgpu_object.h) are both already visible in amdgpu_gem.c's include set.
Bottom line
Real, unbounded, attacker-repeatable resource-exhaustion primitive on the amdgpu render node, inherited from the Linux 4.x-era ioctl ordering, present verbatim in DragonFly's tree. Severity Medium (local DoS, requires GPU render-node access β no privilege, no memory corruption). Fix is a five-line argument check before the allocation.
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 inserts the bo_map-equivalent argument validation before amdgpu_vm_alloc_pts() in the MAP/REPLACE paths; a doomed mapping then fails with -EINVAL before any page-table BO is created, eliminating the stranding. Cannot be validated on this guest (no AMD GPU hardware; amdgpu not in the guest kernel build) β not_testable. The added wrap guards also pre-fix the DF-1719 offset+size wrap for this ioctl path.
findings/poc/DF-4576/fix.diff (git-apply-able vs sys/dev/drm/amd/amdgpu/amdgpu_gem.c, insertion between the bo_va selection and the operation switch); guest env.txt showing why no baseline/patched run is possible
Confirmed kernel references
Detail
Exploit chain
open /dev/dri/renderD128 -> GEM_CREATE 4 KiB GTT BO -> GEM_VA MAP {va>=1 MiB, map_size=span, offset_in_bo=0x1000000000000000} -> alloc_pts allocates all page tables for span -> bo_map rejects -EINVAL -> page tables stay owned by vm->root until fd close -> repeat with new fds until GTT exhausted -> amdgpu allocation failures for every GPU process
Evidence (decisive lines)
run.log (PoC run as unprivileged user on guest: ENOENT, exit 2), build.log (BUILD_RC=0 on DF 6.5-DEVELOPMENT), env.txt (virtio VGA 0x1111:1234, no /dev/dri, amdgpu absent from guest sys/conf/files), VERDICT.md (line-level trace amdgpu_gem.c:637-646/657-666 -> amdgpu_vm.c:945-1023 -> amdgpu_vm.c:2493-2512 -> amdgpu_gem.c:675-680)
PoC changes
seed sketch was only a description; wrote a fully self-contained PoC (Linux-ioctl-encoding macros hand-rolled since DragonFly
Verified recommended fix
Validate map_size/offset_in_bo (alignment, non-zero, va_address+map_size and offset_in_bo+map_size wrap, offset+size <= amdgpu_bo_size) in amdgpu_gem_va_ioctl BEFORE calling amdgpu_vm_alloc_pts, so rejected mappings cannot allocate page tables (see fix.diff)
Verdict
Code-trace-proven: amdgpu_gem_va_ioctl (amdgpu_gem.c:637-646 MAP, :657-666 REPLACE) calls amdgpu_vm_alloc_pts() on the raw va_address/map_size BEFORE amdgpu_vm_bo_map() validates offset_in_bo/map_size; a deliberately-rejected mapping strands ~(map_size>>18) bytes of page-table BOs in the per-fd VM until close (~1 GiB per 256 TiB span ioctl, 4 KiB BO needed). Repeat with fresh render-node fds -> GTT/kernel exhaustion, GPU wedge for all processes (unprivileged, DRM_AUTH|DRM_RENDER_ALLOW). PoC built clean on the DragonFly guest but could not be executed to effect: the QEMU guest has no AMD GPU (virtio VGA only), /dev/dri absent, amdgpu not in the guest kernel build -> status untested (device absent), expected impact on real hardware: dos.
No comments yet.