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).
- Generate an EDID with a preferred mode of
width=16384(or 16384+tiles) andheightlarge enough thatsurface_height = h * drm_fbdev_overalloc/100forcesALIGN(h,8)such that65536 * ALIGN(h,8) mod 2^32is a small positive value — e.g., height that makes the wrap land on 1 MiB. - Boot the DragonFly guest with that display attached (or hot-plug post-boot).
- The
fb_probeatdrm_fb_helper_initial_configallocates a small BO. - Any fbcon redraw (syscons
sckmsrndrkms_draw/kms_blank_fill) computesdraw_pos = info->vaddr + stride*y + x*cppand writes past the BO; if the adjacent KVA is unmapped the kernel panics (verify withdmesgshowing a page fault onrbo->kptr + offset).
Success criterion: kernel panic / fb_write fault, not a clean boot.
Recommended fix
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
sys/dev/drm/radeon/radeon_fb.c:142-158— the overflowsys/dev/drm/drm_modeset_helper.c:88— pitches[0] propagated into fbsys/dev/drm/radeon/radeon_gem.c:47-94—radeon_gem_object_createsize acceptancesys/dev/drm/drm_fb_helper.c:52-53,2015—drm_fbdev_overallocamplifiersys/dev/syscons/sckmsrndr.c:476,496,528,579,669— stride×y consumers
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1968 · 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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).
Recommended fix
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
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- r
- a
- d
- e
- o
- n
- _
- f
- b
- .
- c
- :
- 1
- 4
- 2
- -
- 1
- 5
- 8
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.
No comments yet.