amdgpu atom FB scratch bounds check integer overflow enables heap OOB read/write
- File:
sys/dev/drm/amd/amdgpu/atom.c - Lines: 270, 271, 273, 278, 461, 462, 514, 526, 527, 529, 533, 873, 877
- Severity: High
- CVSS:
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
- Confidence: certain
Summary
The bounds check guarding FB (framebuffer scratch) accesses computes
gctx->fb_base + (idx*4) in uint32 arithmetic and compares against
scratch_size_bytes.
Because fb_base is a fully attacker-controlled 32-bit value (set via
SET_FB_BASE or WS[FB_WINDOW]), choosing fb_base near 0xFFFFFFFF wraps the
sum below scratch_size_bytes, bypassing the guard.
The subsequent scratch[(fb_base/4)+idx] dereference is then a wild
out-of-bounds heap read or write with attacker-controlled value, offset, and
alignment.
Root cause
In atom_get_src_int ATOM_ARG_FB (atom.c:270-281):
idx = U8(*ptr) at atom.c:271 yields 0-255 (idx is uint32_t, declared
atom.c:176).
The guard at atom.c:273 is
if ((gctx->fb_base + (idx * 4)) > gctx->scratch_size_bytes).
gctx->fb_base is uint32_t (atom.h:134) and is set to an arbitrary 32-bit
value from the VBIOS bytecode by atom_op_setfbbase (atom.c:873-878,
ctx->ctx->fb_base = atom_get_src(ctx, attr, ptr); at atom.c:877) and by
WS[ATOM_WS_FB_WINDOW] writes (atom.c:513-515, gctx->fb_base = val; at
atom.c:514).
The addition fb_base + idx*4 is performed in uint32 arithmetic
(atom.c:273) and silently wraps modulo 2^32.
scratch_size_bytes (int, atom.h:142) is promoted to uint32 for the
comparison, so an attacker choosing fb_base=0xFFFFFFFC with idx=1 gets
0xFFFFFFFC + 4 = 0 (wrap), 0 > 20480 is false, the guard is bypassed, and the
access at atom.c:278 gctx->scratch[(gctx->fb_base / 4) + idx] =
scratch[0x3FFFFFFF + 1] = scratch[0x40000000] is a wild OOB heap read.
The identical bug exists in atom_put_dst ATOM_ARG_FB (atom.c:526-535, guard
at atom.c:529, write at atom.c:533), giving a controlled heap OOB write: the
written value val is fully controlled by preceding bytecode.
The scratch buffer is kzalloc'd in amdgpu_atombios.c:1818 /
amdgpu_atomfirmware.c:106 with attacker-influenced size
(VBIOS VRAM_UsageByFirmware.usFirmwareUseInKb, default 20 KB) and the size is
stored in ctx->scratch_size_bytes (amdgpu_atombios.c:1821 /
amdgpu_atomfirmware.c:109).
Threat
Attacker delivers a crafted VBIOS.
The atom interpreter runs in kernel context during GPU init
(amdgpu_atom_asic_init, atom.c:1354) and every mode-set / DP-aux / encoder
operation.
A VBIOS table issues SET_FB_BASE (atom.c:873) to load a near-0xFFFFFFFF
value into fb_base, then a MOVE/MASK/ADD targeting FB[idx] writes an
attacker-chosen 32-bit value at a controlled large offset past the scratch heap
allocation.
This is a precise heap-corruption primitive (controlled address offset via
fb_base, controlled index via idx, controlled value via source operand) that
can overwrite adjacent kernel heap objects β function pointers, refcounts,
free-list metadata β enabling kernel code execution or privilege escalation.
Read variant leaks kernel heap contents.
Same threat model as the recursion finding (evil PCIe card auto-probed at boot; VM passthrough GPU with crafted VBIOS = guest-to-host escape).
Exploit / PoC
Craft a VBIOS command table containing:
SET_FB_BASE(opcode0x39inopcode_tableatatom.c:1128,attr=IMM/DWORD) with immediate value0xFFFFFFFCβctx->fb_base = 0xFFFFFFFC;MOVE_FB(opcode0x04atatom.c:1073,attrencodingdst=FB/DWORD src=IMM/DWORD) with FB index byte0x01and an immediate source dword holding the desired overwrite value (e.g. address of a gadget or crafted function pointer).
On execution: bounds check at atom.c:529 computes
0xFFFFFFFC + 4 = 0 (uint32 wrap) β 0 > 20480 is false β guard passes β
scratch[(0xFFFFFFFC/4)+1] = scratch[0x40000000] writes the controlled dword
~16 GB past the scratch heap buffer.
For a DoS proof, use idx/fb_base such that the accessed address is unmapped
β immediate kernel page-fault panic.
Build the crafted VBIOS as a flat binary, supply to
qemu -device vfio-pci,romfile=evil.rom (or override BAR1 VBIOS) and observe
kernel panic during amdgpu_atom_asic_init.
For heap-grooming escalation, spray kernel heap objects of known layout adjacent to the scratch allocation (display-mode ioctls allocate same-size objects), then use the controlled write to corrupt a victim object's function pointer.
Recommended fix
Validate fb_base independently of idx to prevent the wrap, and use
overflow-safe arithmetic.
--- a/sys/dev/drm/amd/amdgpu/atom.c
+++ b/sys/dev/drm/amd/amdgpu/atom.c
@@ -270,11 +270,16 @@ static uint32_t atom_get_src_int(atom_exec_context *ctx, uint8_t attr,
case ATOM_ARG_FB:
idx = U8(*ptr);
(*ptr)++;
- if ((gctx->fb_base + (idx * 4)) > gctx->scratch_size_bytes) {
+ if (gctx->fb_base >= (uint32_t)gctx->scratch_size_bytes ||
+ (uint64_t)gctx->fb_base + (uint64_t)idx * 4 + 4 >
+ (uint64_t)gctx->scratch_size_bytes) {
DRM_ERROR("ATOM: fb read beyond scratch region: %d vs. %d\n",
gctx->fb_base + (idx * 4), gctx->scratch_size_bytes);
val = 0;
- } else
- val = gctx->scratch[(gctx->fb_base / 4) + idx];
+ } else {
+ val = gctx->scratch[(gctx->fb_base / 4) + idx];
+ }
if (print)
@@ -526,11 +531,16 @@ static void atom_put_dst(atom_exec_context *ctx, int arg, uint8_t attr,
case ATOM_ARG_FB:
idx = U8(*ptr);
(*ptr)++;
- if ((gctx->fb_base + (idx * 4)) > gctx->scratch_size_bytes) {
+ if (gctx->fb_base >= (uint32_t)gctx->scratch_size_bytes ||
+ (uint64_t)gctx->fb_base + (uint64_t)idx * 4 + 4 >
+ (uint64_t)gctx->scratch_size_bytes) {
DRM_ERROR("ATOM: fb write beyond scratch region: %d vs. %d\n",
gctx->fb_base + (idx * 4), gctx->scratch_size_bytes);
- } else
+ } else {
gctx->scratch[(gctx->fb_base / 4) + idx] = val;
+ }
The uint64 intermediate prevents 32-bit wrap, and the separate fb_base
sanity check rejects out-of-range bases outright. Also fixes the off-by-one
(> instead of >=) that allows a 4-byte write exactly at
scratch_size_bytes.
Related findings
- DF-1535 (twin, radeon/atom.c): identical defect in the radeon copy.
- DF-1542/DF-1544/DF-1545 (siblings): other atom interpreter OOB family in this file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1543 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace logic harness: FB scratch u32-wrap OOB read+write (amdgpu, twin of DF-1535) | 1.7 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/atom.c (validated apply + compile) | 1.1 KB | view raw |
| run.log | run-log | full unpatched + patched harness output | 286 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.0 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-1543 β amdgpu atom FB scratch u32 wrap -> heap OOB read+write (twin of DF-1535)
Verdict
REPRODUCED (code-confirmed via harness). Source-trace confirms the bug
at sys/dev/drm/amd/amdgpu/atom.c:270-281 (read); 526-535 (write). 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
Identical to DF-1535 in amdgpu's atom.c. fb_base (u32) + idx*4 wraps mod 2^32, bypassing the > scratch_size_bytes guard. Wild OOB heap access at scratch[(fb_base/4)+idx].
Harness output
scratch_size_bytes=20480 (alloc dwords=5120) access scratch[1073741824] (alloc dwords=5120) RESULT: BUGGY - guard bypassed (0xFFFFFFFC + 4 = 0 wraps to 0 <= 20480) ---PATCHED--- scratch_size_bytes=20480 (alloc dwords=5120) RESULT: PATCHED - u64 guard rejects fb_base=0xfffffffc idx=1
Fix
Cast both operands to uint64_t before comparison.
The full git-apply-able unified diff is in fix.diff. It applies cleanly
to /usr/src/sys/dev/drm/amd/amdgpu/atom.c:270-281 (read); 526-535 (write) 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 (FB scratch u32 wrap OOB simulator (same as DF-1535))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/atom.c and atom.c compiles cleanly via in-guest amdgpu module build.
fix.diff applies clean: 2 hunks at 270, 526 patched module build: cc -c atom.c -> atom.o clean; amdgpu.ko linked clean
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- t
- o
- m
- .
- c
- :
- 2
- 7
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- t
- o
- m
- .
- c
- :
- 2
- 7
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- t
- o
- m
- .
- c
- :
- 5
- 2
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- t
- o
- m
- .
- c
- :
- 5
- 3
- 3
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, primitive is a wild OOB kernel heap read+write at attacker-controlled offset; slab grooming lands it on a victim object. Primitive characterized via source trace + userspace harness; chain written into harness.c (shared with DF-1535).
Evidence (decisive lines)
scratch_size_bytes=20480 (alloc dwords=5120) access scratch[1073741824] (alloc dwords=5120) RESULT: BUGGY - guard bypassed (0xFFFFFFFC + 4 = 0 wraps to 0 <= 20480) ---PATCHED--- RESULT: PATCHED - u64 guard rejects fb_base=0xfffffffc idx=1
PoC changes
Added harness.c (shared FB-scratch-wrap replica with DF-1535). Added build.sh, run.sh, fix.diff (u64 cast at both FB read and write guards in amdgpu atom.c).
Verified recommended fix
Cast both fb_base and (idx*4) to uint64_t at lines 273 and 529 before the > comparison. Full diff in findings/poc/DF-1543/fix.diff; supersedes finding proposal.
Verdict
REPRODUCED. Source-trace at sys/dev/drm/amd/amdgpu/atom.c:270-281 (read) and 526-535 (write) confirms identical u32-wrap guard as DF-1535: ((gctx->fb_base + (idx*4)) > gctx->scratch_size_bytes). fb_base from VBIOS via atom_op_setfbbase (line 877) and WS[FB_WINDOW] (line 514). u32+u32 wraps mod 2^32; fb_base=0xFFFFFFFC + idx=1 = 0 <= 20480 -> bypass. Wild OOB heap read+write at scratch[(fb_base/4)+idx] = scratch[0x40000000].
No comments yet.