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

GPU-reset vs overlay-off lifetime race: intel_overlay_reset() clears overlay->crtc while an in-flight overlay-off retire still dereferences it β€” NULL+offset kernel write or GEM_BUG_ON panic

Summary

intel_overlay_off submits MI_OVERLAY_OFF with a retire callback and sleeps. A WAIT_LOCKED waiter that observes reset handoff executes i915_reset() itself from inside its wait loop, which calls intel_overlay_reset: it frees old_vma (last_flip NOT detached, the off_tail retire still pending) and sets crtc=NULL. When the off request retires, off_tail executes 'crtc->overlay = NULL' with crtc==NULL - kernel write to 0+offset - panic. Alternate interleaving: GEM_BUG_ON in submit_request. Reset half triggerable by an UNPRIVILEGED client hanging the GPU while the master stops Xv; off half is any Xv stop or CRTC dpms disable. Gen2-5 only. Guard-the-deref + retire-before-clear fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4314 Β· 11 files
FileTypeDescriptionSize
trigger.c β€” 4.6 KB view raw
build.sh β€” 140 B view raw
run.sh β€” 125 B view raw
build.log β€” 67 B view raw
run.log β€” 285 B view raw
env.txt β€” 496 B view raw
fix.diff β€” 1.4 KB view raw
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 6.4 KB ↓ raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.1 KB view raw

DF-4314 β€” intel_overlay: GPU-reset vs overlay-off lifetime race β†’ NULL-crtc write / GEM_BUG_ON panic

What

intel_overlay_reset() (invoked from i915_reset()) tears down overlay state (overlay->crtc = NULL, releases old_vma) without regard for a concurrent, still-in-flight intel_overlay_off() β€” whose retire callback intel_overlay_off_tail() then executes overlay->crtc->overlay = NULL with overlay->crtc == NULL (NULL+offset kernel write β†’ page fault β†’ panic), or whose pending last_flip makes the reset's own intel_overlay_release_old_vid() slowpath trip GEM_BUG_ON() in intel_overlay_submit_request() β†’ BUG() β†’ panic.

  • File: sys/dev/drm/i915/intel_overlay.c
  • Lines: 220-221 (GEM_BUG_ON), 364-379 (intel_overlay_off_tail, crash at 373), 435-475 (intel_overlay_release_old_vid), 477-490 (intel_overlay_reset)
  • Reset entry: i915_drv.c:2356 (intel_overlay_reset inside i915_reset), executed from inside a sleeping overlay waiter's own wait loop (i915_request.c:1250-1260 __i915_wait_request_check_and_reset, i915_gem_active_retire uses I915_WAIT_LOCKED, i915_request.h:701-722).

Build / Run (exact)

Guest (audit QEMU, no i915 hardware β€” honest device-absent run):

vm.sh run_root 'cc -O2 -I/usr/src/sys/dev/drm/include -o /tmp/trigger /tmp/trigger.c'
vm.sh run_root '/tmp/trigger /dev/dri/card0'

On real hardware (Gen2–Gen5 Intel GPU with overlay unit):

cc -O2 -I/usr/src/sys/dev/drm/include -o trigger trigger.c
./trigger /dev/dri/card0 &            # DRM-master overlay on/off hammer
# in parallel, as an UNPRIVILEGED user: induce GPU hangs (e.g. endless
# oversized GEM batchbuffers) so i915_reset() fires while an overlay-off
# request is pending; watch for panic in intel_overlay_off_tail / GEM_BUG_ON.

Expected

  • Vulnerable HW: kernel panic β€” either Fatal trap 12: page fault writing 0+offsetof(struct intel_crtc, overlay) from intel_overlay_off_tail, or GEM_BUG_ON(i915_gem_active_peek(...)) β†’ BUG() from intel_overlay_submit_request.
  • Audit guest (this run): trigger compiles clean; open("/dev/dri/card0") β†’ ENOENT; exit 2. The i915 driver cannot attach (Bochs/virtio VGA only β€” env.txt), so the path is unreachable at runtime on this guest.

Artifacts

  • trigger.c β€” DRM-master race half (overlay on/off hammer)
  • build.log, run.log, env.txt β€” full guest compile/run output + PCI/env
  • fix.diff β€” verified git apply --check-clean two-hunk fix
  • VERDICT.md β€” full narrative and code-path proof
  • manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-4314 VERDICT β€” not_reproduced on the audit guest (device absent); race proven structurally in source

Reproduced?

No β€” on this guest it is impossible by construction. The audit QEMU guest has no Intel GPU: pciconf -l shows only vgapci0 chip=0x11111234 (Bochs/VBE virtio VGA), /dev/dri and /dev/drm do not exist (run.log, env.txt), and no drm/i915 module is loaded. HAS_OVERLAY() is never true, intel_setup_overlay() never runs, and the ioctl entry points return -ENODEV β€” in fact the drm2 device never attaches at all. The trigger therefore fails at open() with ENOENT (exit 2). Per the audit status vocabulary this is a device/feature-absent environment; recorded as not_reproduced / reproduced=0 / impact=none for what was observable on this guest, with the code-level analysis below.

Why the bug is real (source-level proof)

The two colliding paths

Path A β€” overlay off (DRM master, e.g. X stopping Xv video):

  1. intel_overlay_put_image_ioctl with !I915_OVERLAY_ENABLE (intel_overlay.c:1075-1085), or a CRTC dpms disable (intel_display.c:5221-5229), calls intel_overlay_switch_off (intel_overlay.c:856).
  2. intel_overlay_off (:382-421) calls intel_overlay_flip_prepare(overlay, NULL) (:417) β€” moves the live vma into overlay->old_vma β€” then submits the MI_OVERLAY_OFF request with retire callback intel_overlay_off_tail and sleeps in intel_overlay_do_wait_request β†’ i915_gem_active_retire (:419-420, i915_request.h:701-722).

Path B β€” GPU reset:

  1. The wait uses I915_WAIT_LOCKED; a waiter that observes reset handoff executes i915_reset() itself from inside its wait loop (i915_request.c:1320-1322 β†’ __i915_wait_request_check_and_reset :1250-1260). Independently, any other WAIT_LOCKED waiter or the error handler can perform the reset.
  2. i915_reset (i915_drv.c:2291, lockdep_assert_held(struct_mutex) holds the mutex Path A dropped around its sleep) calls i915_gem_reset() β€” completing Path A's still-in-flight off request β€” and then intel_overlay_reset(i915) (i915_drv.c:2356).
  3. intel_overlay_reset (intel_overlay.c:477-490) runs intel_overlay_release_old_vid(overlay): - if the DOVSTA flip-pending bit is clear (:449), it frees old_vma via the direct tail call (:471-472) β€” last_flip is not detached, the pending off_tail retire remains registered; - if the bit is set, it takes the slowpath, which calls intel_overlay_submit_request (:216-226) whose GEM_BUG_ON(i915_gem_active_peek(&overlay->last_flip, …)) fires because last_flip still tracks the completed-but-unretired OFF request β†’ BUG() β†’ panic (GEM_BUG_ON is unconditionally fatal: i915_gem.h:42-49). Then it sets overlay->crtc = NULL; overlay->active = false (:488-489).

The crash

  1. Path A's i915_gem_active_retire resumes: the request is now complete, so it detaches last_flip and invokes the retire callback β€” intel_overlay_off_tail (i915_request.h:719: active->retire(active, request); intel_overlay.c:364-379).
  2. off_tail runs intel_overlay_release_old_vma (WARN_ON(!vma) β€” old_vma was already freed in step 5, WARN only, :340-353) and then executes

c overlay->crtc->overlay = NULL; /* intel_overlay.c:373 */

with overlay->crtc == NULL (cleared in step 5) β†’ kernel write to 0 + offsetof(struct intel_crtc, overlay) (an absolute low address, unmapped) β†’ page fault in kernel mode β†’ panic.

There is no NULL guard anywhere on this path, and the alternative retire-worker ordering (Path A aborted earlier by a signal, the OFF request retired by the periodic i915_retire_requests worker after the reset) reaches the identical off_tail NULL dereference. Both interleavings are architecturally enabled by design: WAIT_LOCKED waits exist precisely so the waiter runs the reset inline.

Reachability / attacker model

  • The overlay-off half is DRM-master territory (X stopping Xv, or a CRTC dpms disable β€” kernel-internal, intel_display.c:5228).
  • The reset half is triggerable by an unprivileged local user: GPU hangs from unprivileged GL/GEM clients are the canonical i915 reset trigger, and i915_handle_error/hangcheck then schedules exactly the i915_reset β†’ intel_overlay_reset path raced above. When the GPU hangs, the master's in-flight overlay-off wait blocks until the reset β€” i.e. the "overlay-off pending during reset" condition is the common case whenever video playback/modeset coincides with a hang, not a narrow window.
  • Requires Gen2–Gen5 Intel graphics with the overlay unit (HAS_OVERLAY); Ironlake+ (no overlay) is unaffected.

Impact: local kernel panic (DoS) β€” no controlled data is written (fixed NULL base + fixed small offset), no info leak. Race-condition reliability: high on hang coincidence, but not 100% deterministic β†’ severity Medium, CWE-362/CWE-476.

Fix validation

fix.diff (two hunks) β€” git apply --check clean against this tree:

  1. intel_overlay_off_tail: NULL-guard overlay->crtc (kills crash shape 6-7).
  2. intel_overlay_reset: retire the pending last_flip first, while overlay->crtc is still valid (kills both the deferred-NULL-deref and the GEM_BUG_ON shape β€” release_old_vid afterwards finds old_vma already released by the properly-ordered retire).

fix_status: not_testable β€” validating on this guest is impossible: the driver cannot attach (no Intel GPU), so neither baseline nor patched kernel can execute the path. The fix was instead validated by re-deriving every interleaving (reset-inside-wait, reset-inside-error-handler, deferred retire worker) against the patched source: each ordering now retires off_tail with crtc valid before any NULL assignment.

Artifacts

trigger.c (code_hash a6499d4a5144fd6e4c159a43f8c8db24cdc3a4afac57f53c36da096aed46c7b4), build.log (clean guest compile), run.log (device-absent result), env.txt (guest PCI/DRM inventory), fix.diff, manifest.json, verdict.json.

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 is git-apply-clean against this tree and kills both crash shapes by ordering (retire off_tail while crtc is valid) plus a NULL guard; cannot be runtime-validated on this guest because the i915 driver cannot attach (no Intel GPU). All three interleavings (reset-inside-waiter, reset-inside-error-handler, deferred retire-worker) re-derived against the patched source: each retires off_tail with crtc valid before any NULL assignment.

fix.diff in this pack; 'git apply --check' output captured in run.log lineage (see VERDICT.md Fix validation section).
↓ fix.diffper-fix-DF-4314

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (full interleaving proof with path:line); trigger.c + build.log + run.log (clean guest compile, honest device-absent run: exit 2, ENOENT); env.txt (guest PCI inventory proving no i915); fix.diff (git apply --check clean; two hunks: NULL-guard off_tail + retire-before-clear in intel_overlay_reset)

PoC changes

Wrote the trigger from scratch (no seed): raw ioctl DRM master + GEM create + GETRESOURCES + PUT_IMAGE on/off hammer using the tree's own uapi headers; guest needed -I/usr/src/sys/dev/drm/include and uudecode transfer (no base64 binary on guest).

Verified recommended fix

intel_overlay_reset(): retire the pending last_flip (intel_overlay_recover_from_interrupt) BEFORE clearing overlay state, and NULL-guard overlay->crtc in intel_overlay_off_tail (fix.diff).

Verdict

Race structurally proven in source but NOT runtime-reproducible on the audit guest: the guest has no Intel GPU (only Bochs/virtio VGA, no /dev/dri, no drm module loaded - see env.txt), so the i915 overlay path cannot execute there; the trigger compiles cleanly on the guest and exits 2 with open() ENOENT. On real Gen2-Gen5 i915 hardware: intel_overlay_reset() (called from i915_reset, which a WAIT_LOCKED overlay waiter even executes inline from its own wait loop) clears overlay->crtc and frees old_vma while a concurrent intel_overlay_off() is still in flight; the pending off_tail retire then executes overlay->crtc->overlay = NULL with crtc==NULL (NULL+offset write -> kernel page fault -> panic), or the reset's release_old_vid slowpath trips GEM_BUG_ON (unconditional BUG) in intel_overlay_submit_request because last_flip still tracks the unretired off request. The GPU-reset half is triggerable by an unprivileged client hanging the GPU; the overlay-off half is any Xv stop / CRTC dpms disable. Local DoS panic, Medium.