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

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:

  1. SET_FB_BASE (opcode 0x39, attr=IMM_DWORD) with immediate value 0xFFFFFFFC β€” this sets ctx->fb_base=0xFFFFFFFC;
  2. MOVE_FB (opcode 0x04, 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. 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.

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.

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

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1535 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md verdict human-readable narrative with mechanism + fix
↓ download 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 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 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
↓ fix.diffn/a (module-bound bug; guest has no AMD GPU)

Confirmed kernel references

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.