amdgpu_vram_mgr_new: non-power-of-two pages_per_node breaks the num_nodes bound β one-struct heap OOB write past the kvmalloc_array node array, unprivileged via GEM_CREATE in.alignment
Summary
num_nodes=DIV_ROUND_UP(num_pages,pages_per_node) and the rounddown_pow_of_two split both assume p2 pages_per_node, but mem->page_alignment is user-steerable via AMDGPU_GEM_CREATE in.alignment (only clamped < PAGE_SIZE) and never p2-validated: 3MB alignment -> pages_per_node=768, num_nodes=1, loop 2 writes nodes[1] - a full 184-byte drm_mm_node OOB, linked into the live mm (and removed again by _del - second OOB write). VERIFIED IN KERNEL SPACE on a drm-enabled custom guest kernel: verbatim-loop kld harness wrote start=768 size=256 allocated=1 mm=<kptr> past a 1-element array and clobbered a sprayed neighbor across 13 words incl. kernel pointers; fix loop stayed in bounds. Render-node reachable (DRM_AUTH|DRM_RENDER_ALLOW) on every AMD card with >=4MB VRAM. p2-clamp fix compile+behavior validated (amdgpu.ko relinked -Werror).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5008 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| kernel/vram_oob_harness.c | β | 12.0 KB | view raw | |
| kernel/Makefile | β | 477 B | β download | |
| model/vram_oob_model.c | β | 7.7 KB | view raw | |
| build.sh | β | 158 B | view raw | |
| run.sh | β | 136 B | view raw | |
| run.log | β | 2.2 KB | view raw | |
| model_run.log | β | 8.4 KB | view raw | |
| build.log | β | 1.2 KB | view raw | |
| fix_build.log | β | 1.2 MB | β download | |
| dmesg.txt | β | 8.5 KB | view raw | |
| env.txt | β | 408 B | view raw | |
| fix.diff | β | 971 B | view raw | |
| VERDICT.md | β | 4.8 KB | β raw |
DF-5008 Verification Verdict
Finding: amdgpu_vram_mgr_new() nodes[] out-of-bounds write via
non-power-of-two mem->page_alignment
Severity: High Β· Impact class: kernel heap OOB write (memcorrupt)
Verification method: in-kernel harness executing the verbatim driver loop
against the real in-kernel drm_mm + kvmalloc on a custom drm-enabled guest
kernel, plus a userspace arithmetic sweep. The amdgpu entry point is
hardware-gated (no AMD GPU in QEMU; amdgpu binds only on real ASICs), but
every instruction of the defect β the bound computation and both insert loops
β ran unmodified in kernel space.
Verdict: REPRODUCED (kernel-space primitive; hardware-gated entry)
How the chain was proven
-
Reachability of the attacker input (static, path:line-exact):
AMDGPU_GEM_CREATEis unprivileged on render nodes (amdgpu_kms.c:1197DRM_AUTH|DRM_RENDER_ALLOW);args->in.alignmentflows amdgpu_gem.c:260 β amdgpu_gem.c:58 (no power-of-two check β the only clamp is< PAGE_SIZE) β amdgpu_object.c:434 (roundup(3MB, PAGE_SIZE) >> PAGE_SHIFT= 768, non-p2 preserved) β ttm_bo.c:1247 β ttm_bo.c:1099 β amdgpu_vram_mgr.c:183-185 (pages_per_node = max(512, 768) = 768, non-p2) βnum_nodes = DIV_ROUND_UP(768, 768) = 1β loop 1 (:201-215) insertsnodes[0]withrounddown_pow_of_two(768) = 512pages;pages_left = 256β loop 2 (:217-235) inserts&nodes[1]β out of bounds. -
The sink really writes there (kernel, real drm_mm): S1 β the real
drm_mm_insert_node_in_range()wrote a live node atnodes[1]of a 1-elementkvmalloc_array(run.log):start=768 size=256 allocated=1 color=0 mm=0xfffff8011845f668 hole_size=1047552.sizeof(struct drm_mm_node) = 184on this build, so this is a 184-byte write starting at byte offset 184 of a 184-byte allocation. -
It corrupts foreign objects (kernel, groomed): S2 β with the exact-size array allocated into a hole between sprayed M_DRM neighbors, the OOB node clobbered neighboring object #17 across 13 machine words, including kernel text/heap pointers (
0xfffff8008da72040) and partial-word residue (0xaa55aa5500000001) β cross-object heap corruption in a stock kernel allocator zone (run.log). -
The arithmetic is general (userspace model): every non-p2 alignment
2 MB (i.e. exceeding default
amdgpu_vram_page_split = 512pages) withsize == alignmentoverflows by exactly one node; every p2 alignment is safe; alignments β€ 2 MB are absorbed by the p2 default split (model_run.log). Loop 2 provably runs β€ 1 iteration (its body zeroespages_left), so the overflow is exactly one node β 184 bytes β past the array, linked into the live mm. -
Amplitude/trigger characterization: first allocation on a fresh VRAM mm succeeds deterministically (hole starts at page 0;
0 % 768 == 0; second insert lands at 768..1024 β needs only β₯ 4 MB VRAM). Repeatable per ioctl; content semi-controlled (start/size attacker-steerable, kernel pointers in the rest); the OOB node stays linked inmgr->mmlists untilamdgpu_vram_mgr_del()removes it (a second OOB write of the same struct) andkvfrees the array.
Why not uid=0 / panic on this guest
amdgpu.ko builds cleanly as a loadable module on DragonFly (confirmed:
fix_build.log/DRMTEST build compiled the whole driver, rc=0) but only binds
on AMD ASICs; the QEMU guest has none (std-VGA), so the ioctlβdriver path
cannot be entered here. Escalation to uid=0 on real hardware is the
expected outcome class for a repeatable 184-byte semi-controlled kernel-heap
overflow with linked-list/rbtree kernel-pointer writes (classic slab-groom β
victim object β credential overwrite), but was not demonstrated β stated
explicitly rather than claimed.
Fix validation
fix.diffclampspages_per_nodeto a power of two (rounddown), which restores the loop-1 β₯ pages_per_node invariant sonum_nodesis again a true upper bound (loop 2 β€ 1 node, proven).- Behavioral: harness S3 ran the fixed loop with the real drm_mm β no write
beyond
num_nodes(run.log SUMMARYS3=0), while the unfixed loop (S1/S2) corrupts (S1=1 S2=1). - Compile: fix applied inside the guest's
/usr/srccopy; the amdgpu module rebuilt with-Werror, rc=0 (fix_build.log:amdgpu_vram_mgr.oline 149,amdgpu.korelink line 781). The workspacesys/tree was not modified. - Guest reverted to the clean
with-srcsnapshot afterwards.
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
fixedFixed. Behavioral: harness S3 ran the patched loop (rounddown_pow_of_two clamp) against the real drm_mm with identical attacker params - no write beyond num_nodes (run.log SUMMARY S3=0) while baseline S1/S2 corrupt (S1=1 S2=1). Compile: fix.diff applied to the guest's /usr/src copy, amdgpu_vram_mgr.o rebuilt and amdgpu.ko relinked under -Werror rc=0 (fix_build.log). Workspace sys/ untouched; guest reverted afterwards.
fix_run.log (S3 clean, SUMMARY S1=1 S2=1 S3=0); fix_build.log line 149 ('--- amdgpu_vram_mgr.o ---' recompiled) and line 781 (amdgpu.ko relink); fix.diff (clamps pages_per_node at amdgpu_vram_mgr.c:197)
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_vram_mgr.c:183
- sys/dev/drm/amd/amdgpu/amdgpu_vram_mgr.c:185
- sys/dev/drm/amd/amdgpu/amdgpu_vram_mgr.c:201-215
- sys/dev/drm/amd/amdgpu/amdgpu_vram_mgr.c:217-235
- sys/dev/drm/amd/amdgpu/amdgpu_gem.c:58
- sys/dev/drm/amd/amdgpu/amdgpu_gem.c:260
- sys/dev/drm/amd/amdgpu/amdgpu_object.c:434
- sys/dev/drm/amd/amdgpu/amdgpu_kms.c:1197
- sys/dev/drm/ttm/ttm_bo.c:1247
- sys/dev/drm/ttm/ttm_bo.c:1099
- sys/dev/drm/drm_mm.c:534-570
Detail
Exploit chain
unpriv user opens /dev/dri/renderD* -> AMDGPU_GEM_CREATE{size=3MB, alignment=3MB(non-p2), domains=VRAM} -> amdgpu_gem.c:58 (no p2 validation) -> amdgpu_object.c:434 (768 pages) -> ttm_bo.c:1247,1099 -> amdgpu_vram_mgr.c:185 (pages_per_node=768 non-p2, num_nodes=1) -> loop1 nodes[0]=512p@0, loop2 nodes[1]=256p@768 = 184-byte heap OOB write past kvmalloc_array(1,184), OOB struct linked into live drm_mm (drm_mm.c:556-570); del() writes it again then kvfree. On amdgpu hw: repeatable semi-controlled heap overflow (start/size steerable, kernel ptrs) -> slab groom -> victim object -> uid0 (not demonstrated: guest has no AMD GPU).
Evidence (decisive lines)
run.log: 'S1 ... [!!] 1 node-struct(s) written past the 1-element allocation; nodes[1]: start=768 size=256 allocated=1 ... mm=0xfffff8011845f668'; 'S2 ... neighbor 17 word 1: fffff8008da72040 != magic (CLOBBERED)' (13 words); 'SUMMARY S1=1 S2=1 S3=0'. fix_run.log: S3 fixed loop clean. fix_build.log:149,781 patched amdgpu_vram_mgr.o rebuilt + amdgpu.ko relinked rc=0 -Werror. model_run.log: sweep (non-p2>2MB => +1 node OOB, p2 => safe). env.txt: uname/kldstat.
PoC changes
No seed PoC existed (finding new in pass 2). Harness adaptations of the verbatim loop are marked [ADAPT] in kernel/vram_oob_harness.c and none touch the bound math; model PoC needed a u32 typedef fix and a %llu cast fix after the first guest compile.
Verified recommended fix
amdgpu_vram_mgr.c: clamp pages_per_node = rounddown_pow_of_two(pages_per_node) before DIV_ROUND_UP (fix.diff); companion: reject non-p2 in.alignment in amdgpu_gem.c amdgpu_gem_object_create()
Verdict
REPRODUCED at the primitive level in kernel space. The AMDGPU_GEM_CREATE ioctl accepts a non-power-of-two in.alignment (amdgpu_gem.c:58 only clamps <PAGE_SIZE); a 3MB alignment on a 3MB VRAM BO yields pages_per_node=768 (non-p2, amdgpu_vram_mgr.c:183-185) and num_nodes=DIV_ROUND_UP(768,768)=1, but loop 1 splits rounddown_pow_of_two(768)=512 so loop 2 inserts nodes[1] out of bounds. On a drm-enabled guest kernel (GENERIC+device drm), a kld harness running the verbatim loop with the real drm_mm_insert_node_in_range/kvmalloc demonstrated: S1 a full struct drm_mm_node (184B: mm ptr, start=768, size=256, list/rb pointers) written at byte offset 184 of the 184-byte allocation and linked into the live mm; S2 the exact-size array groomed between sprayed M_DRM neighbors clobbered a foreign neighbor object across 13 words including kernel pointer 0xfffff8008da72040; S3 the fix.diff loop (rounddown_pow_of_two(pages_per_node)) stayed within bounds. Unprivileged render-node reachability is certain from source (DRM_AUTH|DRM_RENDER_ALLOW, amdgpu_kms.c:1197); uid0 escalation on real amdgpu hardware is the expected class for this primitive but was not demonstrable on the GPU-less guest.
No comments yet.