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

DragonFly DIV_ROUND_DOWN_ULL compat shim corrupts drm_rect_clip_scaled(): post-validation source-rectangle corruption feeds OOB display-engine scanout on i915 sprite paths

Summary

The shim defines DIV_ROUND_DOWN_ULL as (x/n)*n instead of Linux's floor x/n - its only in-tree user is clip_scaled, where every clipped 1:1/downscaled update returns a src ~dst times too large (wrapped mod 2^32, effectively attacker-tuned) written into plane_state->src AFTER the DRM core validated the original against the fb. The i915 VLV/CHV/SKL sprite chains never re-validate: DSPSURF offset = y*pitch+x*cpp scans out past the fb (cross-process pixel leak via adjacent GTT memory) and/or display-engine fault. REPRODUCED in the running guest kernel (real stock drm.ko export: 3/3 geometries diverge from Linux semantics, src escapes fb while Linux stays in-fb); one-line shim fix validated on rebuilt drm.ko (3/3 exact Linux semantics).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4891 Β· 11 files
FileTypeDescriptionSize
df4891_recttest.c β€” 6.3 KB view raw
Makefile β€” 266 B ↓ download
build.sh β€” 221 B view raw
run.sh β€” 131 B view raw
build.log β€” 5.7 KB view raw
run.log β€” 1.5 KB view raw
fix.diff β€” 735 B view raw
fix_build.log β€” 61 B view raw
fix_run.log β€” 1.2 KB view raw
VERDICT.md β€” 4.4 KB ↓ raw
verdict.json β€” 4.8 KB view raw
VERDICT.md
↓ download raw

DF-4891 VERDICT β€” REPRODUCED (in-kernel primitive), fix validated

Bottom line

DragonFly's DIV_ROUND_DOWN_ULL(x,n) compat shim (sys/dev/drm/include/linux/kernel.h:61) computes (x/n)*n instead of Linux's do_div-truncated x/n. Its sole in-tree user is clip_scaled() (sys/dev/drm/drm_rect.c:64). Consequently every 1:1 or downscaled plane update that gets clipped by drm_rect_clip_scaled() receives a source rectangle whose width/height are (src*(dst-diff)/dst)*dst mod 2^32 β€” up to ~2^32 units and effectively attacker-chosen β€” written back into plane_state->src (drm_rect.c:90/98/106/114) after the DRM core already validated the original src against the framebuffer (drm_atomic.c:573-586, drm_plane.c:623-636) and without any later fb-bounds re-validation on the i915 sprite paths.

What was executed (guest, stock kernel)

df4891.ko (MODULE_DEPEND on drm) called the real exported drm_rect_clip_scaled() from the guest's stock drm.ko with three unprivileged-SetPlane geometries (1920x1080 fb, 1:1 and 2:1 planes, left/top and right/bottom CRTC clips):

case kernel (stock) src rect Linux semantics verdict
A 1:1 top-left clip x1=y1=INT_MAX, w=h= -30848/-31688 px (inverted) x1=480px,y1=270px, w=1440px,h=810px (in fb) DIVERGENCE
C 1:1 bottom-right clip w=12288px, h=22832px on a 1920x1080 fb w=1440px, h=810px (in fb) DIVERGENCE
D 2:1 downscale left clip x1=INT_MAX (inverted) x1=960px, w=960px (in fb) DIVERGENCE

All three: visible=1 β€” the plane is accepted; the corrupted src is what the driver would program. See run.log.

Sink trace (code-level; hardware absent on this guest)

drm_atomic_helper_check_plane_state() (drm_atomic_helper.c:790) stores the corrupted src into plane_state->src; callers:

  • VLV/CHV sprites: vlv_sprite_check intel_sprite.c:1195 β†’ intel_plane_check_src_coordinates intel_sprite.c:255 (pixel-snap only β€” Case A/D snap to src_x=32767px) β†’ i9xx_check_plane_surface intel_display.c:3312 (no src bounds check) β†’ intel_compute_aligned_offset intel_display.c:2410 (offset = *y * pitch + *x * cpp, u32 wrap) β†’ DSPSURF/DSPADDR/SPSIZE. Case C programs a 12288px-wide viewport on a 1920px-wide fb: the display engine scans ~4x past the fb row into adjacent GTT memory (cross-process pixel leak) and/or faults (DoS).
  • SKL sprites (intel_sprite.c:1334 β†’ skl_check_main_surface intel_display.c:3025): width capped at max_width but x1 never checked against fb bounds / negativity β€” bounded OOB remains.
  • g4x sprites (intel_sprite.c:1138): width_bytes<=4096 check (1095-1113) partially contains; 1:1 early-return path (1090) skips it.
  • Primary planes (can_position=false) and the cursor path (intel_display.c:9826 rejects panned offsets) are contained.

The QEMU guest has no Intel display hardware, so the final scanout-leak pixel capture could not be executed; the corruption primitive itself was executed in the running kernel. Impact ceiling: OOB display-engine read (info leak of other processes' GPU buffers onto the screen) + display-engine fault DoS, reachable by an unprivileged local user holding DRM master.

Fix validation

fix.diff (one line): #define DIV_ROUND_DOWN_ULL(x, n) ((unsigned long long)(x) / (n)). Applied inside the guest's /usr/src; drm.ko rebuilt; fresh boot (vm.sh reset with-src, patch, rebuild, first drm load of the boot β€” the stock drm.ko unload path stalls on this guest, which is why the patched run was done on a clean boot); same test module re-run against the patched drm.ko: no divergence on all three cases, kernel rectangles identical to upstream Linux semantics (fix_run.log). Baseline-vs-patched diff is exactly the finding: corruption present (baseline) / gone (patched).

Notes

  • EXPORT_SYMBOL is a no-op in this tree, so drm_rect_clip_scaled is a plain module global β€” the test kld resolves it via MODULE_DEPEND(drm).
  • Guest was reset (vm.sh reset with-src) after both runs; returned clean.
  • Unrelated observation: kldunload drm on this guest wedges in module teardown (pre-existing, not this finding).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

One-line shim fix restores Linux floor-division semantics. Guest drm.ko rebuilt with fix.diff on a fresh boot: same test module now returns rectangles identical to upstream Linux semantics on all three geometries (no divergence); baseline stock drm.ko showed corruption on all three. Bad behavior gone.

findings/poc/DF-4891/fix_run.log ('no divergence' x3, kernel==LINUX) vs findings/poc/DF-4891/run.log ('DIVERGENCE ... POST-VALIDATION SRC CORRUPTION' x3); fix_build.log shows patched drm.ko link
↓ fix.diffper-fix-DF-4891

Confirmed kernel references

Detail

Exploit chain

unpriv local user w/ DRM master -> SetPlane/atomic ioctl, 1:1 (NO_SCALING) or downscale geometry with plane partially off-CRTC -> drm_atomic_plane_check validates ORIGINAL src (drm_atomic.c:573) -> drm_atomic_helper_check_plane_state:790 -> drm_rect_clip_scaled -> clip_scaled() else-branch returns (tmp/dst)*dst mod 2^32 (attacker-tuned via src/dst/clip) -> plane_state->src corrupted post-validation -> vlv/skl sprite check paths never re-validate fb bounds -> intel_compute_aligned_offset folds src.x1/width into DSPSURF offset + viewport size -> display engine reads past the framebuffer object -> adjacent GTT/aperture memory displayed (cross-process info leak) or engine fault (DoS). In-kernel corruption primitive demonstrated live; hardware sink traced.

Evidence (decisive lines)

run.log lines 'DF4891[C-1to1-bottomright] KERNEL ... w=805306368(12288px)' vs 'LINUX ... w=94371840(1440px)' + 'DIVERGENCE ... POST-VALIDATION SRC CORRUPTION' x3; fix_run.log 'no divergence' x3 after fix.diff; VERDICT.md case table + sink trace

PoC changes

seed was a concept; wrote the real kld (MODULE_DEPEND on drm resolves the no-OP-EXPORT_SYMBOL global), C89-style declarations, sys/types uint32_t instead of linux u32; fixed Case C geometry to hit the else-branch (initial draft hit the correct up-rounding branch); added Linux-semantics reference implementation for side-by-side divergence printing

Verified recommended fix

Fix the shim: DIV_ROUND_DOWN_ULL(x,n) = ((unsigned long long)(x) / (n)) (Linux do_div-truncated quotient); one-line change to sys/dev/drm/include/linux/kernel.h:61, validated on guest

Verdict

DragonFly's DIV_ROUND_DOWN_ULL(x,n) compat shim (sys/dev/drm/include/linux/kernel.h:61) computes (x/n)n instead of Linux's floor(x/n); its only user clip_scaled() (sys/dev/drm/drm_rect.c:64) therefore returns a ~dst-times-too-large, mod-2^32-wrapped new_src_w for every clipped 1:1 or downscaled plane update, corrupting plane_state->src AFTER the DRM core validated the original src against the framebuffer. Called the real drm_rect_clip_scaled() from the guest's stock drm.ko via a test kld: all three unprivileged-SetPlane geometries produced corrupted source rectangles (12288px-wide src on a 1920px fb; INT_MAX-inverted rects) where Linux semantics yield in-fb 1440/810/960px rects, with visible=1 (plane accepted). Sink code-traced end-to-end on i915 (vlv_sprite_check -> i9xx_check_plane_surface has no src bounds check -> offset = ypitch + x*cpp, u32, intel_display.c:2410 -> DSPSURF/viewport programming => OOB display-engine scanout = cross-process pixel leak / engine-fault DoS); final screen-leak capture impossible on the std-VGA QEMU guest (no i915 hardware). One-line shim fix validated on the guest: patched drm.ko returns exactly Linux-semantics rectangles, corruption gone.