Integer overflow in FB scratch bounds check enables heap OOB read/write
- File:
sys/dev/drm/radeon/atom.c - Lines: 281, 284, 285, 286, 289, 540, 543, 544, 545, 547, 852, 856, 528
- 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 compares
gctx->fb_base + (idx * 4) (computed in u32 arithmetic) against
scratch_size_bytes.
Because fb_base is a fully attacker-controlled 32-bit value, choosing
fb_base near 0xFFFFFFFF wraps the sum below scratch_size_bytes, bypassing
the guard.
The subsequent scratch[] dereference at (fb_base/4)+idx is then a wild
out-of-bounds heap read or write with an attacker-controlled value, offset, and
alignment.
Root cause
In atom_get_src_int ATOM_ARG_FB (atom.c:281-292):
idx = U8(*ptr) yields 0β255 (uint32_t idx, line 180).
The guard at line 284 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 by atom_op_setfbbase (atom.c:856:
ctx->ctx->fb_base = atom_get_src(ctx, attr, ptr)) and by
WS[ATOM_WS_FB_WINDOW] writes (atom.c:528: gctx->fb_base = val).
The addition fb_base + idx*4 is performed in uint32 arithmetic and silently
wraps modulo 2^32.
When scratch_size_bytes (int, e.g. 20480) is converted to uint32 for the
comparison, an attacker choosing fb_base=0xFFFFFFFC with idx=1 gets
fb_base+4 = 0x100000000 β wraps to 0, and 0 > 20480 is false, so the guard
is bypassed.
The actual access at line 289
gctx->scratch[(gctx->fb_base / 4) + idx] = scratch[0x3FFFFFFF + 1] is a wild
OOB heap read.
The identical bug exists in atom_put_dst ATOM_ARG_FB (atom.c:540-549, write
at line 547), giving a controlled heap OOB write: the written value val is
fully controlled by preceding bytecode (e.g. a MOVE_IMM to FB).
The FB buffer (gctx->scratch) is kzalloc'd at atom.c:1424 with
attacker-influenced size (from VBIOS VRAM_UsageByFirmware, default 20 KB).
Threat
Attacker delivers a crafted VBIOS.
The atom interpreter runs in kernel context during GPU init and every mode-set.
A VBIOS table issues SET_FB_BASE to load a near-0xFFFFFFFF value into
fb_base, then a MOVE/MASK/ADD operation 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, or
free-list metadata β enabling kernel code execution or privilege escalation.
Read variant leaks kernel heap contents.
Same threat vectors as the recursion finding (evil PCIe card auto-probed at boot; VM passthrough GPU with crafted VBIOS).
Exploit / PoC
Craft a VBIOS command table containing:
SET_FB_BASE(opcode0x39,attr=IMM_DWORD) with immediate value0xFFFFFFFCβ this setsctx->fb_base=0xFFFFFFFC;MOVE_FB(opcode0x04,attrencodingdst=FB/DWORD src=IMM/DWORD) with FB index byte0x01and an immediate source dword holding the desired overwrite value (e.g. an address of a gadget or a crafted function pointer).
On execution: the bounds check at atom.c:543 computes
0xFFFFFFFC + 4 = 0 (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 and fb_base such that the accessed address is
unmapped β immediate kernel page-fault panic.
To demonstrate, build the crafted VBIOS as a flat binary, map it as the GPU ROM
(or pass to a QEMU -device vfio-pci,romfile=evil.rom passthrough), boot, and
observe the kernel panic during atom_asic_init.
For heap-grooming escalation, spray kernel heap objects of known layout adjacent to the scratch allocation, 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/radeon/atom.c
+++ b/sys/dev/drm/radeon/atom.c
@@ -281,9 +281,14 @@ 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];
@@ -540,9 +545,14 @@ 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 {
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.
Related findings
- DF-1534 (sibling): unbounded recursion in
atom_op_calltable. - DF-1536 (sibling): unbounded PS operand index stack OOB.
- DF-1537 (sibling): unbounded WS operand index + NULL deref.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1535 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace logic harness: FB scratch u32-wrap OOB read+write (radeon atom_get_src_int ATOM_ARG_FB) | 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/radeon/atom.c (validated apply + compile) | 1.0 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.4 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-1535 β radeon atom FB scratch u32 wrap -> heap OOB read+write
Verdict
REPRODUCED (code-confirmed via harness). Source-trace confirms the bug
at sys/dev/drm/radeon/atom.c:281-292 (read); 540-549 (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
atom_get_src_int ATOM_ARG_FB guards the scratch access with ((gctx->fb_base + (idx*4)) > gctx->scratch_size_bytes). fb_base is u32, settable from VBIOS via atom_op_setfbbase (line 856) and WS[ATOM_WS_FB_WINDOW] writes (line 528). u32+u32 wraps mod 2^32: fb_base=0xFFFFFFFC + idx=1 -> sum=0 > 20480 is false -> guard bypassed. The actual access scratch[(fb_base/4)+idx] = scratch[0x3FFFFFFF+1] is a wild OOB heap read. The matching write path at atom_put_dst ATOM_ARG_FB (line 540-549) has the identical bug β controlled 32-bit write at controlled OOB offset.
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 the comparison so the wrap cannot bypass the guard.
The full git-apply-able unified diff is in fix.diff. It applies cleanly
to /usr/src/sys/dev/drm/radeon/atom.c:281-292 (read); 540-549 (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 read/write 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 radeon module does not attach on the audit guest. Validated fix.diff applies cleanly to /usr/src/sys/dev/drm/radeon/atom.c and the patched atom.c compiles cleanly via in-guest module build. Harness shows the u64-cast guard rejects fb_base=0xfffffffc idx=1 (vs bypass unpatched).
fix.diff applies clean: 2 hunks at 281, 540 patched module build: cc -c atom.c -> atom.o clean (no errors) harness: unpatched bypasses guard with wrap; --fixed rejects fb_base=0xfffffffc
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- a
- t
- o
- m
- .
- c
- :
- 2
- 8
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- a
- t
- o
- m
- .
- c
- :
- 2
- 8
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- a
- t
- o
- m
- .
- c
- :
- 5
- 4
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- a
- t
- o
- m
- .
- c
- :
- 5
- 4
- 7
Detail
Exploit chain
blocked by valid Phase-6 hard blocker: radeon module does not attach on the audit guest (no AMD GPU). On a host with AMD graphics (or VM passthrough), the primitive is a wild kernel heap OOB read+write at attacker-controlled offset; escalation would groom the slab to land the OOB write on a victim object (function pointer / ops vector / ucred*) and redirect at it. Primitive characterized via source trace + userspace harness; chain written into harness.c.
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--- scratch_size_bytes=20480 (alloc dwords=5120) RESULT: PATCHED - u64 guard rejects fb_base=0xfffffffc idx=1
PoC changes
Added harness.c (replicates the FB scratch guard arithmetic and access indexing). Added build.sh, run.sh, fix.diff (cast both operands to uint64_t before comparison so the wrap cannot bypass).
Verified recommended fix
Cast both fb_base and (idx*4) to uint64_t before the > comparison at lines 284 and 543, so the unsigned-overflow wrap cannot bypass the bound. Full diff in findings/poc/DF-1535/fix.diff; supersedes finding proposal.
Verdict
REPRODUCED. Source-trace at sys/dev/drm/radeon/atom.c:281-292 (read) and 540-549 (write) confirms the FB scratch guard uses 32-bit arithmetic: ((gctx->fb_base + (idx*4)) > gctx->scratch_size_bytes). fb_base is u32, settable from VBIOS via atom_op_setfbbase (line 856) AND WS[ATOM_WS_FB_WINDOW] writes (line 528). u32+u32 wraps mod 2^32: fb_base=0xFFFFFFFC + idx=1 -> sum=0 > 20480 is false -> guard bypassed. The access scratch[(fb_base/4)+idx] = scratch[0x3FFFFFFF+1] is a wild OOB heap read; matching write at line 547 is a controlled OOB write. Harness replicates the math and shows the patched u64-cast guard rejects fb_base=0xFFFFFFFC.
No comments yet.