u32 truncation of nptes in amdgpu_vm_bo_update_mapping undersizes the update IB; amdgpu_vm_update_ptes then emits an unbounded command stream past its end (kernel heap overflow)
Summary
'unsigned nptes' truncates a u64 page count mod 2^32. A single-call range spanning >= 2^32 GPU pages (16 TiB) - reachable from clear_freed (whole freed mapping) or BO-less PRT mappings - wraps nptes (to 0 for an exact 2^32 span), sizing the IB at ~100 dwords while update_ptes walks the full range emitting write_pte packets with no capacity check -> overflow of the kmalloc'd IB. Honest gating: default block_size 9 makes the giant mapping infeasible (OOM in PT BOs); with vm_block_size>=16 (root tunable) it needs only ~64K PTs and is unprivileged-triggerable via PRT MAP+UNMAP - would be High on such configs. Linux v4.20/v5.0 carry the identical truncation; eliminated by the 5.3 VM rework. 64-bit sizing fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3863 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 2.8 KB | β raw | |
| va_nptes_overflow.c | β | 2.3 KB | view raw | |
| build.sh | β | 132 B | view raw | |
| run.sh | β | 179 B | view raw | |
| build.log | β | 219 B | view raw | |
| run.log | β | 201 B | view raw | |
| env.txt | β | 483 B | view raw | |
| fix.diff | β | 757 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 3.1 KB | view raw |
DF-3863 β 32-bit truncation of nptes in amdgpu_vm_bo_update_mapping() undersizes the update IB β unbounded PTE-command writes overflow the IB buffer
File: sys/dev/drm/amd/amdgpu/amdgpu_vm.c β amdgpu_vm_bo_update_mapping() (1797-1933)
Root cause
amdgpu_vm.c:1807 declares unsigned nptes, ncmds, ndw; (32-bit) and
amdgpu_vm.c:1841 computes
nptes = last - start + 1; /* uint64_t arithmetic, truncated to u32 */
start/last are GPU page numbers of the range being written. When a single
update covers β₯ 2^32 GPU pages (16 TiB), nptes wraps:
- nptes β‘ 0 (span exactly 2^32) β ncmds = 1, ndw β 64 + 10 + frag*β¦
β a ~100-dword IB is allocated (amdgpu_vm.c:1879);
- amdgpu_vm_update_ptes() (amdgpu_vm.c:1915 β 1670-1777) then walks the
page-table leaves of the range and emits amdgpu_vm_write_pte /
set_pte_pde packets into job->ibs[0].ptr with no capacity check
(the only guard is the after-the-fact WARN_ON(params.ib->length_dw > ndw)
at amdgpu_vm.c:1920) β kernel heap buffer overflow of the kmalloc'd IB,
one WRITE_PTE packet per 2 MB of the span.
Which callers can hand a β₯2^32-page range to update_mapping in ONE call:
- amdgpu_vm_clear_freed() (amdgpu_vm.c:2333-2335) passes the entire
freed mapping span (mapping->start .. mapping->last);
- amdgpu_vm_bo_split_mapping() with nodes == NULL (BO-less PRT
mapping β created via AMDGPU_VA_OP_MAP with AMDGPU_VM_PAGE_PRT, where the
ioctl uses fpriv->prt_va whose base.bo is NULL, amdgpu_gem.c:604-631)
sets max_entries = S64_MAX (amdgpu_vm.c:2006-2007) so the full mapping
span again lands in a single call.
Reachability (honest gating)
Creating the giant mapping requires amdgpu_vm_alloc_pts() over the whole
span first (amdgpu_gem.c:638-641). With the default block_size = 9
(2 MB leaf coverage) a 16 TiB span needs 2^23.5 page tables β infeasible
OOM. With amdgpu_vm_block_size β₯ 16 (root module parameter; GMC9 default
VM size is 256 TB β gmc_v9_0.c:940 amdgpu_vm_adjust_size(adev, 256*1024, 9, 3, 48)
β so the VA space itself allows it) a 16 TiB PRT mapping needs only 2^16
leaves (256 MB of PT BOs): feasible, and then UNMAPping it
(AMDGPU_VA_OP_UNMAP β vm->freed β amdgpu_gem_va_update_vm β
amdgpu_vm_clear_freed) hits the truncated nptes and overflows the IB.
Linux β€5.0 upstream carries the same unsigned nptes (verified against
v4.20 and v5.0 sources); the 5.3+ VM rework removed the whole
pre-size-the-IB-from-total-PTE-count design.
Unprivileged trigger given the config: any render-node user issuing GEM_VA MAP (PRT) + UNMAP of a 2^32-page span.
Trigger
va_nptes_overflow.c β performs the PRT MAP/UNMAP via DRM_IOCTL_AMDGPU_GEM_VA
on /dev/dri/renderD128. Requires amdgpu.vm_block_size=16 (and recovery of
a suitably large vm size, default 256 TB on VEGA10) on hardware.
Fix
fix.diff: uint64_t nptes, ncmds, ndw; + cap the IB request (fail loudly
on absurd sizes instead of truncating).
DF-3863 VERDICT β not_reproduced (hardware-gated + config-gated); truncation certain at source
Runtime
Guest has no AMD GPU (/dev/dri absent β env.txt); the GEM_VA ioctl does not
exist here. status = not_reproduced, reproduced = 0.
Source-level certainty of the sink
- amdgpu_vm.c:1807
unsigned nptes, ncmds, ndw; - amdgpu_vm.c:1841
nptes = last - start + 1;(u64 β u32 truncation) - amdgpu_vm.c:1850-1879
ncmds/ndwderived from the truncated nptes;amdgpu_job_alloc_with_ib(adev, ndw * 4, &job)sizes the IB. - amdgpu_vm.c:1915
amdgpu_vm_update_ptes(¶ms, start, last + 1, ...)receives the FULL u64 range; its loop (1670-1777) emits one packet set per page-table leaf;amdgpu_vm_write_pte/set_pte_pde(amdgpu_vm.h:292-293) append toib->ptr[]without any bounds check. The only guard is post-hoc:WARN_ON(params.ib->length_dw > ndw)(amdgpu_vm.c:1920) β i.e. the overflow has already happened. - Single-call giant ranges reach this function from:
- amdgpu_vm.c:2333-2335 (
amdgpu_vm_clear_freedβ whole freed mapping), - amdgpu_vm.c:2003-2007 + 2037-2040 (
split_mappingwithnodes == NULL, i.e. the BO-less PRT mapping pathfpriv->prt_va, amdgpu_gem.c:630-631,max_entries = S64_MAX).
Gating analysis (why not reproduced and how narrow it is)
- VM size: default on VEGA10+ is 256 TB (gmc_v9_0.c:940) β max_pfn = 2^36 pages > 2^32 β the address space allows a 2^32-page mapping.
- PT footprint:
amdgpu_vm_alloc_ptsmust cover the whole span (invoked in the ioctl beforeamdgpu_vm_bo_map, amdgpu_gem.c:638-641). With default block_size 9 that is ~8.4M PT BOs β allocation fails long before the mapping exists β bug not reachable in default config. - With
amdgpu_vm_block_size >= 16(root-set loader tunable; amdgpu_device.c:819-832 validates only the lower bound 9) a 16 TiB span needs β€ 2^16 leaf PTs (~256 MB VRAM) β feasible; the MAP(PRT)+UNMAP pair then drives the truncatednptes. - Linux v4.20 and v5.0 carry the identical
unsigned nptes(verified from upstream sources); the design was replaced in the 5.3 VM rework. Filed Low due to the configuration gate; severity would be High (unprivileged heap overflow with attacker-influenced packet contents) on a config where block_size β₯ 16.
Fix validation
fix.diff (u64 math + capped IB alloc) git apply --check clean. Cannot
build/exercise on this guest: 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_testablefix.diff switches the sizing math to u64 and caps the alloc; git apply --check clean; not exercisable without AMD hardware
findings/poc/DF-3863/fix.diff
Confirmed kernel references
Detail
Exploit chain
amdgpu.vm_block_size=16 + render node: GEM_VA MAP PRT 16TiB at 64GiB, then UNMAP -> clear_freed -> nptes wraps -> undersized kmalloc'd IB -> update_ptes writes attacker-range-sized packet stream past the IB -> adjacent slab corruption (packet dwords are GPU write commands; PTE payload values partially user-influenced)
Evidence (decisive lines)
VERDICT.md (truncation math + reachability analysis incl. why default config blocks it); build.log/run.log/env.txt
PoC changes
authored from scratch; ioctl ordinal left symbolic with pointer to amdgpu_drm.h since no hardware to finalize against
Verified recommended fix
Make nptes/ncmds/ndw uint64_t in amdgpu_vm_bo_update_mapping and cap/fail the IB allocation explicitly (fix.diff)
Verdict
Sink certain, reachability config-gated: amdgpu_vm.c:1807 declares 'unsigned nptes, ncmds, ndw' and :1841 truncates 'nptes = last - start + 1' (u64 math -> u32); a >=2^32-GPU-page (16TiB) single-call range (reachable from amdgpu_vm_clear_freed :2333 for a whole freed mapping, or split_mapping with nodes==NULL :2006 for BO-less PRT mappings, ioctl path amdgpu_gem.c:604-631) wraps nptes to 0, sizes the IB at ~100 dwords (:1879), while amdgpu_vm_update_ptes (:1915) walks the full range emitting unchecked write_pte packets (guard is only the post-hoc WARN_ON at :1920) -> kernel heap IB overflow. Trigger requires a 16TiB mapping whose PTs must first be allocated by alloc_pts (ioctl :638): with default block_size 9 that is ~8.4M PT BOs (infeasible); with amdgpu_vm_block_size>=16 (root tunable; VM size default 256TB on VEGA10 per gmc_v9_0.c:940) it is ~64K PTs (feasible). Linux v4.20/v5.0 carry the identical 'unsigned nptes' (verified from upstream); removed by the 5.3 VM rework. Filed Low due to the config gate. Guest has no AMD GPU: trigger compiles (RC=0) and runs to the hardware gate (run.log).
No comments yet.