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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4974 Β· 10 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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 only width > max_cursor_width || height > max_cursor_height (radeon_cursor.c:295-299), then pins the object in VRAM via radeon_bo_pin_restricted() and stores cursor_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) with w,h = user-supplied width/height (≀ 64x64). The display engine fetches w*h*4 bytes from cursor_addr.
  • sys/dev/drm/radeon/radeon_cursor.c:241-243 β€” pre-AVIVO path adds yorigin * 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 to cursor_bo; old bo unpinned + put once; pin arithmetic symmetric even when the same handle is set twice, via the pin_count early-return in radeon_object.c:333-334).
  • radeon_bo_reserve failure on the old bo (radeon_cursor.c:350-354): leaves pin_count elevated and drops the last ref β†’ the BO is freed by TTM anyway; worst case rdev->vram_pin_size accounting 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/move crtc derefs: crtc comes from drm_crtc_find; crtc_id default-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_testable
baseline no→ patch + rebuild →patched clean

fix.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/)
↓ fix.diffper-fix-DF-4974

Confirmed kernel references

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.