radeon_crtc_cursor_set2 never validates cursor BO size β display engine scans up to 16 KiB from an arbitrarily small pinned BO (VRAM OOB read to screen)
Summary
Only w/h <= 64 are checked; the fetch window programs w*h*4 (up to 16KiB, + yorigin*256 pre-AVIVO) with no minimum BO size - a 1-page BO makes the scanout read 12KiB past the end and paint adjacent VRAM (other users' framebuffer content on multiseat/shared-GPU) as cursor pixels. DRM_MASTER-gated (the seat owner is unpriv). radeon uses the legacy cursor path with none of the universal-cursor core validation. Size-check-before-pin fix. Hw-gated (no GPU on guest).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4974 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| cursor_oob.c | β | 2.5 KB | view raw | |
| build.sh | β | 65 B | view raw | |
| run.sh | β | 89 B | view raw | |
| build.log | β | 197 B | view raw | |
| run.log | β | 365 B | view raw | |
| VERDICT.md | β | 4.6 KB | β raw | |
| README.md | β | 869 B | β raw | |
| fix.diff | β | 1.1 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-4974 β radeon cursor BO size not validated (display-engine VRAM OOB read)
Build
./build.sh # cc -Wall -Wextra -o cursor_oob cursor_oob.c
Run
./run.sh # on a machine WITH a radeon KMS card, as the seat owner:
# open /dev/dri/card0, DRM_IOCTL_MODE_CURSOR2 with a
# 1-page VRAM GEM bo, width=64 height=64
# Expected on real HW: cursor renders 16 KiB of pixels (12 KiB read past
# the BO end) β adjacent VRAM appears as garbage cursor pixels.
# On this audit guest (no GPU): exit=1, "No such file or directory".
Files
- cursor_oob.c β trigger (device probe + raw ioctl)
- build.sh / run.sh, build.log / run.log
- VERDICT.md β full narrative + killed candidates
- fix.diff β proposed upstream fix (never applied to sys/)
- manifest.json / verdict.json
DF-4974 VERDICT
Bottom line
Not reproduced on this guest β the guest has no radeon GPU (no /dev/dri at
all). The finding is a statically proven missing validation, with impact
confined to real radeon hardware: the display engine fetches up to
64*64*4 = 16 KiB starting at the pinned cursor BO's VRAM address while the
driver never requires the BO to be that large, so an undersized BO makes the
cursor scanout read adjacent VRAM and paint it on screen. status:
not_reproduced, reproduced: 0, impact: none (on this guest). Confidence
in the missing check itself: certain (code); in the OOB scanout: likely
(inferred from the programmed CUR_SIZE / CUR_OFFSET registers).
Root cause (path:line)
sys/dev/drm/radeon/radeon_cursor.c:301-316βradeon_crtc_cursor_set2()looks up any GEM handle (drm_gem_object_lookup), checks onlywidth > max_cursor_width || height > max_cursor_height(radeon_cursor.c:295-299), then pins the object in VRAM viaradeon_bo_pin_restricted()and storescursor_addr. There is no check that the BO covers the cursor image (width*height*4, up to 16 KiB) β or any minimum size at all.sys/dev/drm/radeon/radeon_cursor.c:215-224βradeon_cursor_move_locked()programs the fetch window:CUR_SIZE = ((w-1)<<16) | (h-1)withw,h= user-supplied width/height (β€ 64x64). The display engine fetchesw*h*4bytes fromcursor_addr.sys/dev/drm/radeon/radeon_cursor.c:241-243β pre-AVIVO path addsyorigin * 256(up to ~16 KiB more) to the fetch base address, so even small cursors can fetch past a 1-page BO on legacy ASICs.
Contrast with the universal-cursor path (drm_plane.c:831-846) which wraps the
handle in a framebuffer whose creation validates object size against
pitch*height β radeon does not use that path: it registers only legacy
.cursor_set2/.cursor_move funcs (radeon_display.c:661-662) and no cursor
plane (drm_crtc_init, radeon_display.c:679, passes no planes), so
crtc->cursor == NULL in drm_mode_cursor_common (drm_plane.c:935-938) and
the raw user width/height/handle flow straight into the driver with no
core-side validation.
Reachability
DRM_IOCTL_MODE_CURSOR / MODE_CURSOR2 are DRM_MASTER-gated
(sys/dev/drm/drm_ioctl.c:659,680) β reachable by the unprivileged user who
owns the graphical seat (console/X session gets DRM master), which matches the
audit context. Requires radeon KMS hardware.
Why not reproduced / why not uid=0
This is not a memory-corruption primitive: the OOB read is performed by the
display engine in VRAM and the data surfaces as cursor pixels on the physical
screen β an information-exposure primitive (adjacent VRAM, e.g. other
windows'/users' framebuffer content on multiseat), not a kernel-memory write.
No escalation chain exists from it. The guest (QEMU/KVM dfbsd 6.5-DEVELOPMENT
X86_64_GENERIC) has no GPU: open("/dev/dri/card0") β ENOENT, no drm/radeon
modules, sysctl hw.drm unknown (run.log). Fix validation on hardware is
therefore impossible here; the proposed fix is a pure input-validation clamp
with no behavioral risk.
Killed/considered while here (negative results with weight)
- Refcount/lifetime balance in
radeon_crtc_cursor_set2β every path balances (lookup ref transferred tocursor_bo; old bo unpinned + put once; pin arithmetic symmetric even when the same handle is set twice, via thepin_countearly-return in radeon_object.c:333-334). radeon_bo_reservefailure on the old bo (radeon_cursor.c:350-354): leavespin_countelevated and drops the last ref β the BO is freed by TTM anyway; worst caserdev->vram_pin_sizeaccounting drifts (consumed only by radeon_gem.c:65,232-234 reporting/suballoc caps). Benign; not filed.- width==0 / height==0 / hot_x overflow (radeon_cursor.c:332-333, 147, 219-224):
sink is
(w-1)<<16-style MMIO register garbage, upstream-identical code, display-only effect. No kernel memory write. Not filed. radeon_show/hide/movecrtc derefs: crtc comes fromdrm_crtc_find;crtc_iddefault-cases return early (radeon_cursor.c:81-82,132-133). Clean.- Pin-restricted early-return on an already-pinned (e.g. GTT) BO ignoring the 27-bit legacy restriction: would need a user BO pre-pinned in GTT β no such user-reachable path. Killed.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff adds the minimum-size rejection after the single GEM lookup (refcount-safe); cannot be kernel-built/validated on this guest because the radeon device does not exist here and the bug cannot be triggered at all (baseline not reproducible).
findings/poc/DF-4974/fix.diff (authored post-verification, never applied to sys/)
Confirmed kernel references
- sys/dev/drm/radeon/radeon_cursor.c:295
- sys/dev/drm/radeon/radeon_cursor.c:301
- sys/dev/drm/radeon/radeon_cursor.c:314
- sys/dev/drm/radeon/radeon_cursor.c:218
- sys/dev/drm/radeon/radeon_cursor.c:224
- sys/dev/drm/radeon/radeon_cursor.c:241
- sys/dev/drm/radeon/radeon_display.c:661
- sys/dev/drm/radeon/radeon_display.c:679
- sys/dev/drm/drm_plane.c:935
- sys/dev/drm/drm_ioctl.c:659
Detail
Evidence (decisive lines)
run.log (ENOENT + module/sysctl absence on guest); VERDICT.md (full static trace with path:line and killed candidates); cursor_oob.c (trigger: tiny VRAM GEM bo + CURSOR2 64x64)
PoC changes
Seed sketch rewrote as a self-contained probe with locally-defined drm_mode_cursor2 struct and ioctl number taken from sys/dev/drm/include/uapi/drm/drm.h (0xBB, 'd'); added empirical device-absence documentation (open ENOENT, kldstat, sysctl hw.drm).
Verified recommended fix
Reject cursor BOs smaller than max_cursor_widthmax_cursor_height4 in radeon_crtc_cursor_set2 before pinning (see fix.diff).
Verdict
Statically proven missing validation: radeon_crtc_cursor_set2 (sys/dev/drm/radeon/radeon_cursor.c:301-316) pins an arbitrary-size user GEM BO as the cursor with only a width/height<=max check (radeon_cursor.c:295-299) and never requires the BO to cover the fetch window the hardware is programmed for (CUR_SIZE at radeon_cursor.c:218-224 -> wh4 bytes, up to 64644=16KiB; pre-AVIVO adds yorigin*256 at radeon_cursor.c:241-243). radeon uses the legacy cursor path (radeon_display.c:661-662, drm_crtc_init with no cursor plane -> drm_plane.c:935-938 dispatch) which performs no core-side size validation, unlike the universal-plane path that wraps the handle in a size-checked framebuffer. On real radeon HW a DRM master (unpriv seat owner; ioctl is DRM_MASTER-gated, drm_ioctl.c:659/680) can supply a 1-page VRAM BO with width=height=64 and the display engine scans 12KiB past the BO end, painting adjacent VRAM contents as cursor pixels (info exposure of other VRAM, e.g. other users' window contents). Not reproducible on this guest: QEMU has no GPU β open(/dev/dri/card0) -> ENOENT, no drm/radeon modules, hw.drm absent (run.log). Not a memory-corruption primitive (display-engine VRAM read only), so no uid=0 chain exists.
No comments yet.