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

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:

  1. SET_FB_BASE (opcode 0x39 in opcode_table at atom.c:1128, attr=IMM/DWORD) with immediate value 0xFFFFFFFC β†’ ctx->fb_base = 0xFFFFFFFC;
  2. MOVE_FB (opcode 0x04 at atom.c:1073, attr encoding dst=FB/DWORD src=IMM/DWORD) with FB index byte 0x01 and 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.

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.

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

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1543 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md verdict human-readable narrative with mechanism + fix
↓ download 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 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/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
↓ 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, 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].