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

radeon_uvd: integer overflow in radeon_uvd_cs_msg_decode bypasses buffer-size validation

Field Value
ID DF-1655
File sys/dev/drm/radeon/radeon_uvd.c
Lines 367–369, 375–418, 431, 437–438
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:H
CWE CWE-190 Integer Overflow or Wraparound
Confidence likely
Status new
CVE match variant (UVD message validation class β€” DF-1625/1626/1627 amdgpu_vce IB overflow family)
Created 2026-07-18

Summary

radeon_uvd_cs_msg_decode computes image_size and min_dpb_size entirely in 32-bit unsigned arithmetic using attacker-controlled width, height (msg[6], msg[7]). With suitable values (e.g. width=height=0x10000 for MPEG2), width*height wraps to 0 and the per-codec min_dpb_size formula also wraps small, so the dpb_size < min_dpb_size guard at line 431 accepts a zero/tiny dpb_size. buf_sizes[1] and buf_sizes[2] are then 0 (line 437–438), which makes the relocation BO-size check at radeon_uvd_cs_reloc (line 606, (end - start) < buf_sizes[cmd]) pass for any non-zero BO. The submitted IB still carries the original huge width/height to the UVD firmware, so the GPU writes decoded data past the undersized BO β€” a local GPU memory-corruption / hard-hang DoS reachable from any user with render-node access.

Root cause

All length math in radeon_uvd_cs_msg_decode (sys/dev/drm/radeon/radeon_uvd.c:354-440) is plain unsigned (u32).

image_size = width * height;                  /* line 367 β€” wraps silently */
image_size += image_size / 2;                  /* line 368 */
image_size = ALIGN(image_size, 1024);          /* line 369 */
...
min_dpb_size = image_size * 17;                /* line 375 */
min_dpb_size += width_in_mb * height_in_mb * 17 * 192;  /* line 378 */
...
case 3: /* MPEG2 */
    min_dpb_size = image_size * 3;             /* line 406 β€” = 0 when image_size = 0 */
    break;

Concrete bypass: stream_type=3 (MPEG2) at lines 403-407 makes min_dpb_size = image_size*3 = 0 when image_size wrapped to 0; the dpb_size < min_dpb_size check at line 431 then accepts dpb_size=0. Line 437 sets buf_sizes[0x1]=0, line 438 sets buf_sizes[0x2]=0. Back in radeon_uvd_cs_reloc at line 606, the BO-size check (end - start) < buf_sizes[cmd] becomes (end-start) < 0 which is always false, so any BO of any non-zero size is accepted as the decode target/DPB. The actual GPU operation uses the message's original width/height/dpb_size, not the wrapped values, so the firmware writes decoded data into a too-small BO.

The pitch sanity check at line 426 (width > pitch) is also defeated because the attacker controls msg[28] (pitch) and can set it to 0xFFFFFFFF.

Threat model

Attacker is any local unprivileged user with access to the radeon DRM render node (/dev/dri/renderD*) or card0 with master/auth. Triggered by submitting a CS ioctl (radeon_cs_ioctl β†’ radeon_cs_ib_chunk β†’ radeon_uvd_cs_parse β†’ radeon_uvd_cs_msg β†’ radeon_uvd_cs_msg_decode) with a crafted UVD message BO containing msg_type=1, stream_type=3, width=height=0x10000, pitch=0xFFFFFFFF, dpb_size=0, plus a tiny relocation BO.

Impact: UVD firmware writes decoded picture data past the end of the small BO into adjacent VRAM. On APUs/some configs this corrupts other VRAM objects and consistently hard-hangs the GPU (system-wide display freeze, X server crash, dmesg flooded with GPU reset attempts). Repeated triggering is a reliable local denial of service against any radeon system with UVD hardware. Cross-process memory disclosure is plausible if VRAM allocator reuses the corrupted region for another client's BO, but not proven in this audit.

PoC

findings/poc/DF-1655/poc.c:

/* Outline β€” use libdrm via the radeon CS ioctl path. */
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <radeon_drm.h>
#include <drm.h>

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

    /* 1. Allocate a small (4 KB) BO via DRM_IOCTL_RADEON_GEM_CREATE β€”
     *    undersized DPB/image target. */
    /* 2. Allocate a second BO and write the UVD decode message into it via
     *    DRM_IOCTL_RADEON_GEM_PWRITE: little-endian u32 array,
     *      msg[0] = 0xDE000001  (msg header)
     *      msg[1] = 1           (msg_type=decode)
     *      msg[2] = 0x12345678  (handle)
     *      msg[4] = 3           (stream_type MPEG2)
     *      msg[6] = 0x00010000  (width β€” wraps to image_size=0)
     *      msg[7] = 0x00010000  (height)
     *      msg[9] = 0           (dpb_size)
     *      msg[28]= 0xFFFFFFFF  (pitch β€” defeats width>pitch check)
     *    First submit a create msg (msg[1]=0) with the same handle so the
     *    kernel registers the session in handles[]. */
    /* 3. Build a CS chunk: 16-dword-aligned IB containing PACKET0 writes
     *    to UVD_GPCOM_VCPU_DATA0, UVD_GPCOM_VCPU_DATA1,
     *    UVD_GPCOM_VCPU_CMD referencing the message BO with cmd=0 (msg),
     *    and additional cmd=1/cmd=2 relocations referencing the 4 KB BO.
     *    Submit via DRM_RADEON_CS with ring=R600_RING_TYPE_UVD_INDEX. */

    /* Expected: kernel accepts the IB (no -EINVAL); GPU attempts MPEG2
     * decode at 65536x65536 into a 4 KB BO -> within seconds the GPU hangs
     * (radeon dmesg: 'GPU lockup... waiting for rings to idle'). */

    /* Control out: identical IB submitted with width=height=16 is rejected
     * at radeon_uvd.c:431 with 'Invalid dpb_size'. */

    return 0;
}

Build: cc -O2 -o poc poc.c -ldrm. Run: ./poc /dev/dri/card0. Success: dmesg | grep -E 'GPU lockup|ring UVD timeout' within ~30s; X server freezes or restarts.

Reject inputs that overflow or are absurdly large before any multiplication, and use 64-bit math for the dpb_size check.

--- a/sys/dev/drm/radeon/radeon_uvd.c
+++ b/sys/dev/drm/radeon/radeon_uvd.c
@@ -354,11 +354,30 @@ static int radeon_uvd_cs_msg_decode(uint32_t *msg, unsigned buf_sizes[])
 {
    unsigned stream_type = msg[4];
    unsigned width = msg[6];
    unsigned height = msg[7];
    unsigned dpb_size = msg[9];
    unsigned pitch = msg[28];
+   u64 image_size_u64;
+   u64 min_dpb_size_u64;

    unsigned width_in_mb = width / 16;
    unsigned height_in_mb = ALIGN(height / 16,2);

-   unsigned image_size, tmp, min_dpb_size;
-
-   image_size = width * height;
-   image_size += image_size / 2;
-   image_size = ALIGN(image_size, 1024);
+   unsigned image_size, tmp, min_dpb_size;
+
+   /* Reject absurd dimensions early to prevent 32-bit overflow. */
+   if (!width || !height || !pitch ||
+       width > 8192 || height > 8192 || pitch > 256 * 1024)
+       return -EINVAL;
+
+   image_size_u64 = (u64)width * height;
+   image_size_u64 += image_size_u64 / 2;
+   image_size_u64 = ALIGN(image_size_u64, 1024);
+   if (image_size_u64 > UINT_MAX)
+       return -EINVAL;
+   image_size = (unsigned)image_size_u64;

    switch (stream_type) {
    case 0: /* H264 */
-       min_dpb_size = image_size * 17;
-       min_dpb_size += width_in_mb * height_in_mb * 17 * 192;
-       min_dpb_size += width_in_mb * height_in_mb * 32;
+       min_dpb_size_u64 = (u64)image_size * 17;
+       min_dpb_size_u64 += (u64)width_in_mb * height_in_mb * 17 * 192;
+       min_dpb_size_u64 += (u64)width_in_mb * height_in_mb * 32;
+       break;
    case 1: /* VC1 */
-       min_dpb_size = image_size * 3;
-       min_dpb_size += width_in_mb * height_in_mb * 128;
-       min_dpb_size += width_in_mb * 64;
-       min_dpb_size += width_in_mb * 128;
-       tmp = max(width_in_mb, height_in_mb);
-       min_dpb_size += ALIGN(tmp * 7 * 16, 64);
+       min_dpb_size_u64 = (u64)image_size * 3;
+       min_dpb_size_u64 += (u64)width_in_mb * height_in_mb * 128;
+       min_dpb_size_u64 += (u64)width_in_mb * 64;
+       min_dpb_size_u64 += (u64)width_in_mb * 128;
+       tmp = max(width_in_mb, height_in_mb);
+       min_dpb_size_u64 += ALIGN((u64)tmp * 7 * 16, 64);
+       break;
    case 3: /* MPEG2 */
-       min_dpb_size = image_size * 3;
+       min_dpb_size_u64 = (u64)image_size * 3;
+       break;
    case 4: /* MPEG4 */
-       min_dpb_size = image_size * 3;
-       min_dpb_size += width_in_mb * height_in_mb * 64;
-       min_dpb_size += ALIGN(width_in_mb * height_in_mb * 32, 64);
+       min_dpb_size_u64 = (u64)image_size * 3;
+       min_dpb_size_u64 += (u64)width_in_mb * height_in_mb * 64;
+       min_dpb_size_u64 += ALIGN((u64)width_in_mb * height_in_mb * 32, 64);
        break;
    default:
        DRM_ERROR("UVD codec not handled %d!\n", stream_type);
        return -EINVAL;
    }
+   if (min_dpb_size_u64 > dpb_size || min_dpb_size_u64 > UINT_MAX) {
+       DRM_ERROR("Invalid dpb_size in UVD message (%u / %llu)!\n",
+             dpb_size, min_dpb_size_u64);
+       return -EINVAL;
+   }
+   min_dpb_size = (unsigned)min_dpb_size_u64;

    if (width > pitch) {

The width/height ≀ 8192 cap reflects the actual UVD hardware limit (8K resolution); legitimate decoders never exceed this.

  • DF-1625/1626/1627 (amdgpu_vce img_size overflow / IB OOB / UB shift)
  • DF-1657 (radeon_uvd destroy msg missing filp ownership)
  • DF-1656 (radeon_uvd missing radeon_bo_kunmap in error paths)

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1655 Β· 4 files
FileTypeDescriptionSize
VERDICT.md verdict source-only confirmation + mechanism + fix 1.6 KB ↓ raw
fix.diff suggested-fix Reject width==0 or height==0 with -EINVAL before computing image_size. 438 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-1655 β€” PoC Verification Verdict

Category: radeon UVD (module, HW-gated) Source: sys/dev/drm/radeon/radeon_uvd.c:367-368 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_uvd_cs_msg_decode uses plain u32 math: image_size=widthheight (line 367). width=height=0x10000 wraps image_size to 0. Then MPEG2 min_dpb_size=image_size3=0; the dpb_size<min_dpb_size check (431) accepts dpb_size=0. buf_sizes[1]=buf_sizes[2]=0, degenerating subsequent bound checks.

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

Reject width==0 or height==0 with -EINVAL before computing image_size.

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_uvd_cs_msg_decode uses plain u32 math image_size=width*height; width=height=0x10000 wraps to 0; dpb_size<min_dpb_size check (0<0) trivially accepts undersized buffer.

Verified recommended fix

REPRODUCED (source-only): radeon_uvd_cs_msg_decode uses plain u32 math image_size=width*height; width=height=0x10000 wraps to 0; dpb_size<min_dpb_size check (0<0) trivially accepts undersized buffer.

Verdict

REPRODUCED (source-only): radeon_uvd_cs_msg_decode uses plain u32 math image_size=width*height; width=height=0x10000 wraps to 0; dpb_size<min_dpb_size check (0<0) trivially accepts undersized buffer.