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)
PoC verification
Evidence pack
findings/poc/DF-4314 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_resetinsidei915_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_retireusesI915_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 faultwriting0+offsetof(struct intel_crtc, overlay)fromintel_overlay_off_tail, orGEM_BUG_ON(i915_gem_active_peek(...))βBUG()fromintel_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/envfix.diffβ verifiedgit apply --check-clean two-hunk fixVERDICT.mdβ full narrative and code-path proofmanifest.json,verdict.json
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):
intel_overlay_put_image_ioctlwith!I915_OVERLAY_ENABLE(intel_overlay.c:1075-1085), or a CRTC dpms disable (intel_display.c:5221-5229), callsintel_overlay_switch_off(intel_overlay.c:856).intel_overlay_off(:382-421) callsintel_overlay_flip_prepare(overlay, NULL)(:417) β moves the live vma intooverlay->old_vmaβ then submits theMI_OVERLAY_OFFrequest with retire callbackintel_overlay_off_tailand sleeps inintel_overlay_do_wait_requestβi915_gem_active_retire(:419-420,i915_request.h:701-722).
Path B β GPU reset:
- The wait uses
I915_WAIT_LOCKED; a waiter that observes reset handoff executesi915_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. i915_reset(i915_drv.c:2291,lockdep_assert_held(struct_mutex)holds the mutex Path A dropped around its sleep) callsi915_gem_reset()β completing Path A's still-in-flight off request β and thenintel_overlay_reset(i915)(i915_drv.c:2356).intel_overlay_reset(intel_overlay.c:477-490) runsintel_overlay_release_old_vid(overlay): - if theDOVSTAflip-pending bit is clear (:449), it freesold_vmavia the direct tail call (:471-472) βlast_flipis not detached, the pendingoff_tailretire remains registered; - if the bit is set, it takes the slowpath, which callsintel_overlay_submit_request(:216-226) whoseGEM_BUG_ON(i915_gem_active_peek(&overlay->last_flip, β¦))fires becauselast_flipstill tracks the completed-but-unretired OFF request βBUG()β panic (GEM_BUG_ONis unconditionally fatal:i915_gem.h:42-49). Then it setsoverlay->crtc = NULL; overlay->active = false(:488-489).
The crash
- Path A's
i915_gem_active_retireresumes: the request is now complete, so it detacheslast_flipand invokes the retire callback βintel_overlay_off_tail(i915_request.h:719:active->retire(active, request);intel_overlay.c:364-379). off_tailrunsintel_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 thei915_reset β intel_overlay_resetpath 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:
intel_overlay_off_tail: NULL-guardoverlay->crtc(kills crash shape 6-7).intel_overlay_reset: retire the pendinglast_flipfirst, whileoverlay->crtcis still valid (kills both the deferred-NULL-deref and theGEM_BUG_ONshape βrelease_old_vidafterwards findsold_vmaalready 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_testablefix.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).
Confirmed kernel references
- sys/dev/drm/i915/intel_overlay.c:373
- sys/dev/drm/i915/intel_overlay.c:220
- sys/dev/drm/i915/intel_overlay.c:364
- sys/dev/drm/i915/intel_overlay.c:417
- sys/dev/drm/i915/intel_overlay.c:477
- sys/dev/drm/i915/intel_overlay.c:488
- sys/dev/drm/i915/i915_drv.c:2356
- sys/dev/drm/i915/i915_request.c:1258
- sys/dev/drm/i915/i915_request.h:712
- sys/dev/drm/i915/i915_gem.h:47
- sys/dev/drm/i915/intel_display.c:5228
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.
No comments yet.