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

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

  1. Verify: sysconsctrl -m or ioctl(FBIO_GETLINEWIDTH) returns 0 (truncated stride).
  2. Open /dev/vga0 (requires root or appropriate group on most configs).
  3. mmap large offsets: mmap(fd, PAGE_SIZE, PROT_READ, MAP_SHARED, 0, offset) for offset values from 0 to large.
  4. With stride=0, the bound check ap->a_offset > SIZE_MAX - PAGE_SIZE always passes.
  5. atop(vtophys(vaddr + offset)) returns physical page numbers for addresses far beyond the framebuffer.
  6. 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.

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1985 Β· 4 files
FileTypeDescriptionSize
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
README.md readme PoC trigger description
↓ download 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.

VERDICT.md verdict verification narrative
↓ download raw

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.

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

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.