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)
PoC verification
Evidence pack
findings/poc/DF-4891 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_checkintel_sprite.c:1195 βintel_plane_check_src_coordinatesintel_sprite.c:255 (pixel-snap only β Case A/D snap to src_x=32767px) βi9xx_check_plane_surfaceintel_display.c:3312 (no src bounds check) βintel_compute_aligned_offsetintel_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
x1never 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_SYMBOLis a no-op in this tree, sodrm_rect_clip_scaledis a plain module global β the test kld resolves it viaMODULE_DEPEND(drm).- Guest was reset (
vm.sh reset with-src) after both runs; returned clean. - Unrelated observation:
kldunload drmon 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
fixedOne-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
Confirmed kernel references
- sys/dev/drm/include/linux/kernel.h:61
- sys/dev/drm/drm_rect.c:53
- sys/dev/drm/drm_rect.c:64
- sys/dev/drm/drm_rect.c:85
- sys/dev/drm/drm_rect.c:106
- sys/dev/drm/drm_atomic_helper.c:775
- sys/dev/drm/drm_atomic_helper.c:790
- sys/dev/drm/drm_atomic.c:562
- sys/dev/drm/drm_plane.c:622
- sys/dev/drm/i915/intel_sprite.c:1195
- sys/dev/drm/i915/intel_sprite.c:255
- sys/dev/drm/i915/intel_display.c:3312
- sys/dev/drm/i915/intel_display.c:2410
- sys/dev/drm/i915/intel_display.c:3025
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.
No comments yet.