DragonFlyBSD Kernel Audit
← triage · dashboard
DF-1968

Integer overflow in radeon fbcon size math yields undersized GEM object with stale full pitch

  • File: sys/dev/drm/radeon/radeon_fb.c
  • Lines: 142–156 (overflow at 154)
  • Severity: Medium
  • CVSS 3.1: CVSS:3.1/AV:P/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
  • CWE: CWE-190 Integer Overflow or Wraparound
  • Confidence: likely
  • Status: new

Summary

radeonfb_create_pinned_object computes the backing GEM object size as size = mode_cmd->pitches[0] * height (radeon_fb.c:154) in 32-bit arithmetic (u32 pitches[0] × int height → u32, stored into int size), with no overflow check. The product can wrap to 0 or a small positive value; the subsequent allocation succeeds with a too-small buffer while drm_helper_mode_fill_fb_struct (called via radeon_framebuffer_init at radeon_display.c:1302) stores the original full pitches[0] into fb->pitches[0]. No code path re-validates pitches[0]*height against the actual BO size, so any later access that uses the stored pitch and a Y coordinate computes offsets outside the BO.

Root cause

At radeon_fb.c:142-143 the locals are int aligned_size, size; and int height = mode_cmd->height;. pitches[0] is u32 (set at radeon_fb.c:149 from radeon_align_pitch).

At radeon_fb.c:153 height is replaced with ALIGN(mode_cmd->height, 8) for R600+ (also int, can grow).

At radeon_fb.c:154 size = mode_cmd->pitches[0] * height; performs u32 × u32 → u32 (the int operand is converted per C Usual Arithmetic Conversions), then the u32 result is stored into int size — silent wraparound with no check.

radeon_fb.c:155 aligned_size = ALIGN(size, PAGE_SIZE) propagates the wrong value.

radeon_fb.c:156-158 passes aligned_size (int) to radeon_gem_object_create() whose size parameter is unsigned long (radeon_gem.c:47); a small positive wrapped value passes the size > max_size guard (radeon_gem.c:66) and a too-small BO is created.

drm_framebuffer_init (drm_framebuffer.c:731-757) does not call framebuffer_check (that lives in drm_mode_addfb2 only, drm_framebuffer.c:308), and even framebuffer_check (drm_framebuffer.c:208-209) only rejects pitch*height > UINT_MAX, never validates against obj size.

drm_helper_mode_fill_fb_struct (drm_modeset_helper.c:88) copies mode_cmd->pitches[0] verbatim into fb->pitches[0].

The memset_io at radeon_fb.c:268 is bounded by radeon_bo_size(rbo), so it is safe — but every other consumer (syscons sckmsrndr.c:476/496/528/579/669 via info->stride derived from fb->pitches[0]; CRTC scanout address programming) recomputes offsets from the stored pitch × y and can run past the BO.

Threat model

Attacker supplies a crafted monitor EDID (physical DP/HDMI/DVI plug, USB-C DP alt-mode, or malicious Thunderbolt dock) declaring a mode whose (aligned pitch) × (aligned height) mod 2³² wraps to a small positive value.

Concretely: cpp=4, width chosen so radeon_align_pitch() returns 65536 (width ≥ 16384 on AVIVO), and height = ALIGN(vdisplay,8) such that 65536 × height wraps nonzero — reachable because drm_fb_helper.c:2015 multiplies surface_height by drm_fbdev_overalloc (compile-time 100, runtime-tunable up to 200 via the drm.fbdev_overalloc module param, drm_fb_helper.c:52-53), so surface_height can be up to 2× vdisplay and exceed 65536.

Result: a small (e.g., 64 KiB–4 MiB) BO backs a framebuffer the kernel describes as e.g. 64 KiB-pitched × 64K-tall. Any fbcon redraw (sckmsrndr.c) or scanout setup that uses stride×y offsets writes/reads past the BO into adjacent kernel KVA.

Realistic end-state is kernel panic on unmapped KVA (reliable local DoS from a hotplug event); silent corruption into adjacent VRAM/GTT kernel mappings is plausible but not demonstrated. Not remotely triggerable (no network path), and radeon CRTC max pipe width may cap width below 16384 on most radeon families, narrowing reachability.

Proof of concept

Requires a malicious display (EDID spoofer on DP, or qemu-edid generator in a VM exposing a radeon/emulated GPU).

  1. Generate an EDID with a preferred mode of width=16384 (or 16384+tiles) and height large enough that surface_height = h * drm_fbdev_overalloc/100 forces ALIGN(h,8) such that 65536 * ALIGN(h,8) mod 2^32 is a small positive value — e.g., height that makes the wrap land on 1 MiB.
  2. Boot the DragonFly guest with that display attached (or hot-plug post-boot).
  3. The fb_probe at drm_fb_helper_initial_config allocates a small BO.
  4. Any fbcon redraw (syscons sckmsrndr kms_draw/kms_blank_fill) computes draw_pos = info->vaddr + stride*y + x*cpp and writes past the BO; if the adjacent KVA is unmapped the kernel panics (verify with dmesg showing a page fault on rbo->kptr + offset).

Success criterion: kernel panic / fb_write fault, not a clean boot.

Make the size math overflow-checked and 64-bit, and validate the resulting size against the GEM object after creation.

--- a/sys/dev/drm/radeon/radeon_fb.c
+++ b/sys/dev/drm/radeon/radeon_fb.c
@@ -140,8 +140,8 @@ static int radeonfb_create_pinned_object(struct radeon_fbdev *rfbdev,
    bool fb_tiled = false; /* useful for testing */
    u32 tiling_flags = 0;
    int ret;
-   int aligned_size, size;
-   int height = mode_cmd->height;
+   u64 size, aligned_size;
+   u32 height = mode_cmd->height;
    u32 cpp;

    cpp = drm_format_plane_cpp(mode_cmd->pixel_format, 0);
@@ -152,11 +152,17 @@ static int radeonfb_create_pinned_object(struct radeon_fbdev *rfbdev,

    if (rdev->family >= CHIP_R600)
        height = ALIGN(mode_cmd->height, 8);
-   size = mode_cmd->pitches[0] * height;
-   aligned_size = ALIGN(size, PAGE_SIZE);
+   size = (u64)mode_cmd->pitches[0] * height;
+   if (size > UINT_MAX) {
+       DRM_ERROR("radeonfb: pitch*height %llu overflows\n", size);
+       return -EINVAL;
+   }
+   aligned_size = ALIGN(size, PAGE_SIZE);
    ret = radeon_gem_object_create(rdev, aligned_size, 0,
                       RADEON_GEM_DOMAIN_VRAM,
                       0, true, &gobj);
@@ -159,6 +165,11 @@ static int radeonfb_create_pinned_object(struct radeon_fbdev *rfbdev,
        pr_err("failed to allocate framebuffer (%d)\n", aligned_size);
        return -ENOMEM;
    }
+   if (radeon_bo_size(gem_to_radeon_bo(gobj)) < size) {
+       DRM_ERROR("radeonfb: BO size %lu < required %llu\n",
+             (unsigned long)radeon_bo_size(gem_to_radeon_bo(gobj)), size);
+       radeonfb_destroy_pinned_object(gobj);
+       return -EINVAL;
+   }
    rbo = gem_to_radeon_bo(gobj);

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1968 · 4 files
FileTypeDescriptionSize
README.md readme PoC trigger description 396 B ↓ raw
VERDICT.md verdict verification narrative 971 B ↓ raw
fix.diff suggested-fix git-apply-able fix 602 B view raw
fix_build_summary.txt build-log combined 16-finding kernel build rc=0 826 B view raw
README.md readme PoC trigger description
↓ download raw

DF-1968 PoC

See the parent finding markdown at findings/DF-1968-*.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.

Trigger summary is in the "Proof of concept" section of the finding markdown.

VERDICT.md verdict verification narrative
↓ download raw

DF-1968 Verification

Verdict

SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME (HW/module gated).

The cited defect exists in the audited source at sys/dev/drm/radeon/radeon_fb.c:142-158. radeon is not in GENERIC and requires real radeon GPU hardware not present in the audit QEMU/KVM guest.

Mechanism (source-only confirmation)

radeonfb_create_pinned_object computes size = mode_cmd->pitches[0] * height at L154 in 32-bit arithmetic (u32 × int → u32 → int), no overflow check. Pitch from radeon_align_pitch can be up to 65536 (width>=16384 cpp=4); height amplified by ALIGN(.,8) and drm_fbdev_overalloc (up to 200). Product wraps to small positive → undersized GEM BO while fb->pitches[0] stores full pitch → OOB in later stride×y consumers (syscons sckmsrndr).

Make size/aligned_size u64, compute size=(u64)pitches[0]*height, reject if

UINT_MAX before allocating.

The full git apply-able diff lives in fix.diff in this folder.

Confirmed kernel references

Detail

Exploit chain

none (HW/module gated: OOB write primitive requires radeon HW + crafted EDID)

Evidence (decisive lines)

Combined kernel build: 16 fix.diffs applied, make -j6 nativekernel => rc=0, 0 warnings, 0 errors.

PoC changes

Created VERDICT.md, fix.diff (64-bit overflow check with u64 _tmp), manifest.json, env.txt, build.sh, run.sh.

Verified recommended fix

Add a 64-bit overflow check: compute (u64)pitches[0]*height, reject if > UINT_MAX before allocating. Supersedes finding proposal (simpler).

Verdict

SOURCE-CONFIRMED (HW/module gated). radeonfb_create_pinned_object (radeon_fb.c:142-158): size = mode_cmd->pitches[0] * height at L154 in 32-bit (u32 x int -> u32 -> int), no overflow check. Pitch up to 65536 (width>=16384 cpp=4); height amplified by ALIGN(.,8) + drm_fbdev_overalloc (up to 200). Product wraps -> undersized BO while fb->pitches[0] stores full pitch -> OOB in stride*y consumers. Confirmed by source trace. Not runnable: radeon module, no radeon HW.