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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3863 Β· 11 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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/ndw derived from the truncated nptes; amdgpu_job_alloc_with_ib(adev, ndw * 4, &job) sizes the IB.
  • amdgpu_vm.c:1915 amdgpu_vm_update_ptes(&params, 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 to ib->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_mapping with nodes == NULL, i.e. the BO-less PRT mapping path fpriv->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_pts must cover the whole span (invoked in the ioctl before amdgpu_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 truncated nptes.
  • 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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3863

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).