u32 to uint16 truncation of pitch into fb_info.stride enables syscons mmap SIZE_MAX bound-check bypass (kernel memory read primitive)
- File:
sys/dev/drm/amd/amdgpu/amdgpu_fb.c - Lines: 263 (DragonFly block); consumer at
sys/dev/syscons/syscons.c:4089-4106 - Severity: Medium
- CVSS 3.1:
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U:C:H/I:N/A:N - CWE: CWE-197 Numeric Truncation Error
- Confidence: likely
- Status: new
- Related: DF-1972 (radeon_fb.c identical-class bug), DF-1972 also notes the same i915 site
Summary
At line 263, fb->pitches[0] (u32) is assigned to info->stride
(uint16_t, per framebuffer.h:55). For display modes where the computed
pitch equals or exceeds 65536 (e.g., 16384-pixel-wide display at 32bpp β
pitch = 16384*4 = 65536), stride silently truncates to 0.
This causes the syscons mmap bound check at syscons.c:4091 to compute
sz = roundup(height * 0, PAGE_SIZE) = 0, making sz - PAGE_SIZE underflow
to SIZE_MAX on 64-bit, bypassing the check and allowing userspace to mmap
arbitrary offsets beyond the framebuffer.
Root cause
amdgpu_fb.c line 263:
info->stride = fb->pitches[0];
fb->pitches[0] is u32 (drm_mode.h:512). info->stride is uint16_t
(framebuffer.h:55). The assignment silently truncates.
Pitch computation path
amdgpu_align_pitch(adev, mode_cmd->width, cpp, fb_tiled) at line 146 returns
ALIGN(width, pitch_mask) * cpp.
For cpp=4 (32bpp), pitch_mask=63:
pitch = ALIGN(width, 64) * 4
For width=16384, cpp=4:
ALIGN(16384, 64) = 16384 pitch = 16384 * 4 = 65536 info->stride = (uint16_t)65536 = 0
syscons mmap handler
syscons.c:4089-4106:
size_t sz = roundup(scp->sc->fbi->height * scp->sc->fbi->stride, PAGE_SIZE);
// stride=0 -> height * 0 = 0 -> roundup(0, 4096) = 0
if (ap->a_offset > sz - PAGE_SIZE) {
// sz=0, PAGE_SIZE=4096: '0 - 4096' as size_t underflows to 0xFFFFFFFFFFFFF000
// so 'ap->a_offset > 0xFFFFFFFFFFFFF000' is FALSE for any reasonable offset
return EINVAL; // NEVER taken
} else {
ap->a_result = atop(vtophys(scp->sc->fbi->vaddr + ap->a_offset));
// translates arbitrary kernel virtual addresses to physical
}
The same truncation also affects info->width (line 261) and info->height
(line 262), both uint16_t, but typical display dimensions fit in 16 bits.
The i915 driver (intel_fbdev.c:251) and radeon driver (radeon_fb.c:273)
have the identical truncation.
Threat model
Attacker position: local attacker with access to /dev/vga* (or the
syscons mmap path via /dev/ttyv*).
Trigger: mmap the framebuffer device with an offset far beyond the actual
framebuffer, translating arbitrary kernel virtual addresses to physical page
numbers via vtophys().
This is a physical memory disclosure: the attacker can read kernel
text/data, other processes' memory, crypto keys, or any other physical memory
that the kernel page tables map near the framebuffer's vaddr.
Precondition: a display mode with pitch >= 65536 (width >= 16384 at
32bpp).
AMD GPUs since Southern Islands (Tahiti, 2012) support CRTC scanout up to 16384 horizontal pixels. A 16K display, a malicious DisplayPort EDID override, or a virtual display with a crafted mode can trigger this.
Note: on DragonFly, /dev/vga* permissions vary β on many configs it is
world-unreadable (root-only), but the bug is present regardless.
Proof of concept
Preconditions
amdgpu GPU with a connected display (or virtual display) configured at
width=16384, 32bpp. This causes pitch=65536 β stride=0 in fb_info.
Steps
- Verify:
sysconsctrl -morioctl(FBIO_GETLINEWIDTH)returns 0 (truncated stride). - Open
/dev/vga0(requires root or appropriate group on most configs). mmaplarge offsets:mmap(fd, PAGE_SIZE, PROT_READ, MAP_SHARED, 0, offset)for offset values from 0 to large.- With
stride=0, the bound checkap->a_offset > SIZE_MAX - PAGE_SIZEalways passes. atop(vtophys(vaddr + offset))returns physical page numbers for addresses far beyond the framebuffer.- Read the mapped pages to disclose physical memory contents.
Alternative trigger without physical 16K display
Override EDID via sysctl/devfs to present a 16384-wide mode to the DRM connector, then force fbdev re-init via hotplug event.
Success criterion: mmap of offsets beyond the framebuffer's actual size
(amdgpu_bo_size) succeeds and returns readable physical memory pages
containing kernel data.
Recommended fix
Primary fix: widen stride (and width/height) to uint32_t in
struct fb_info (framebuffer.h). Interim fix in amdgpu_fb.c: reject the
mode if pitch exceeds UINT16_MAX:
--- a/sys/dev/drm/amd/amdgpu/amdgpu_fb.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_fb.c
@@ -258,6 +258,13 @@ static int amdgpufb_create(struct drm_fb_helper *helper,
#ifdef __DragonFly__
info->width = sizes->fb_width;
+ if (fb->pitches[0] > USHRT_MAX) {
+ DRM_ERROR("amdgpu fbdev pitch %u exceeds uint16_t stride; "
+ "display too wide for syscons fb_info.stride\n",
+ fb->pitches[0]);
+ ret = -EINVAL;
+ goto out;
+ }
info->height = sizes->fb_height;
info->stride = fb->pitches[0];
This rejects the fbdev creation for ultra-wide displays instead of silently
truncating. The proper long-term fix is to change uint16_t stride/width/height/depth
in struct fb_info (framebuffer.h:53-56) to uint32_t, which also fixes
intel_fbdev.c:251 and radeon_fb.c:273.
A separate hardening fix belongs in syscons.c:4089-4092 to guard against
sz == 0: if (sz == 0 || ap->a_offset > sz - PAGE_SIZE) return EINVAL; β
but that is out of scope for this file.
References
sys/dev/drm/amd/amdgpu/amdgpu_fb.c:263β the truncating assignmentsys/platform/pc64/include/framebuffer.h:53-55βfb_infofield widthssys/dev/syscons/syscons.c:4089-4106β underflow + unchecked mmap offsetsys/dev/drm/amd/amdgpu/amdgpu_fb.c:146βamdgpu_align_pitch(computes 65536 for width=16384 @ cpp=4)- AMD Southern Islands+ CRTC supports up to 16384 horizontal pixels
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1985 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | PoC trigger description | 317 B | β raw |
| VERDICT.md | verdict | verification narrative | 909 B | β raw |
| fix.diff | suggested-fix | git-apply-able fix | 655 B | view raw |
| fix_build_summary.txt | build-log | combined 16-finding kernel build rc=0 | 826 B | view raw |
DF-1985 PoC
See the parent finding markdown at findings/DF-1985-*.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.
DF-1985 Verification
Verdict
SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME (HW/module gated).
The cited defect exists in the audited source at sys/dev/drm/amd/amdgpu/amdgpu_fb.c:263.
amdgpu is not in GENERIC and requires real AMD GPU hardware.
Mechanism (source-only confirmation)
amdgpu_fb.c:263 info->stride = fb->pitches[0] narrows u32 to uint16_t. For width=16384 cpp=4 amdgpu_align_pitch returns 65536 β stride wraps to 0. syscons.c:4091 sz=roundup(height*stride, PAGE_SIZE); stride=0 β sz=0; L4092 "a_offset > sz - PAGE_SIZE" underflows size_t to SIZE_MAX (always false) β bounds check defeated; L4097 returns atop(vtophys(vaddr + arbitrary a_offset)) for mmap β arbitrary physical page exposure.
Recommended fix
Add an overflow check: if fb->pitches[0] > 0xffff, return -EINVAL before the truncating assignment.
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
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- f
- b
- .
- c
- :
- 2
- 6
- 3
Detail
Exploit chain
none (HW/module gated: mmap bounds bypass requires AMD GPU with large-pitch mode)
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 (add 0xffff overflow check before stride assignment), manifest.json, env.txt, build.sh, run.sh.
Verified recommended fix
Add overflow check: if fb->pitches[0] > 0xffff return -EINVAL before the truncating assignment. Matches finding proposal.
Verdict
SOURCE-CONFIRMED (HW/module gated). amdgpu_fb.c:263 info->stride = fb->pitches[0] narrows u32 to uint16_t. For width=16384 cpp=4 amdgpu_align_pitch returns 65536 -> stride wraps to 0. syscons.c:4091 sz=roundup(height*stride,PAGE_SIZE); stride=0->sz=0; L4092 bound underflows to SIZE_MAX (defeated); L4097 returns atop(vtophys(vaddr+arbitrary a_offset)) -> arbitrary physical page exposure via mmap. Confirmed by source trace. Not runnable: amdgpu module, no AMD HW.
No comments yet.