CT teardown frees the shared CT page while GuC interrupts are still enabled and guc->handler still points at CT code β interrupt-context use-after-free read+write of the released descriptor mapping
Summary
guc_disable_communication calls intel_guc_ct_disable (which frees/unmaps the CT page) BEFORE gen9_disable_guc_interrupts (mask+synchronize_irq) and before the handler swap - a GuC event asserted inside the window dispatches through CT code that reads desc->head/tail and WRITES desc->head/is_in_error through the freed mapping; the ctch_is_open guard is an unlocked check-then-use. Info: unreachable on this tree, privileged lifecycle timing only. Quiesce-before-free ordering fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4568 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| fix.diff | β | 1.4 KB | view raw | |
| verdict.json | β | 3.7 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw |
DF-4568 β intel_guc_ct.c: CT teardown frees the shared CT page while GuC interrupts are still enabled and the handler still points at CT code
- File:
sys/dev/drm/i915/intel_guc_ct.c(ordering root cause shared withintel_uc.c) - Severity: Info (hardening β see reachability)
- Confidence: certain (teardown order statically provable)
What the defect is
Tearing the CT channel down races against the GuC interrupt handler:
guc_disable_communication()(sys/dev/drm/i915/intel_uc.c:226-237) callsintel_guc_ct_disable(&guc->ct)at intel_uc.c:231 β beforegen9_disable_guc_interrupts(i915)at intel_uc.c:233 (which is the call that masks the GuC events andsynchronize_irq()s them, i915_irq.c:555-567).- Inside
intel_guc_ct_disable()(intel_guc_ct.c:887-904), the shared CT page is deregistered and freed/unmapped viactch_close():898 βctch_fini():290 βi915_vma_unpin_and_release(&ctch->vma, I915_VMA_RELEASE_MAP)(intel_guc_ct.c:214-218). Only after the free, at :901-902, areguc->send/guc->handlerswitched to the nop versions. - A GuC event that is already asserted (or arrives) in that window
dispatches
guc->handlerβ stillintel_guc_to_host_event_handler_ct(intel_guc_ct.c:846-851) β which runsct_process_host_channel()(:815-840) on the freed page:ctb_read()dereferencesctb->desc(:581) and writesdesc->head = head * 4(:628); on-EPROTOit also writesctb->desc->is_in_error = 1(:838).
That is a use-after-free read+write (through the released i915 VMA mapping)
in interrupt context. The ctch_is_open(ctch) guard at :822 is
check-then-use against a pointer that i915_vma_unpin_and_release clears
without any lock shared with the IRQ path, so it does not close the window.
Why it is only Info on this tree
Same double unreachability as DF-4567: no platform in this tree sets
info.has_guc_ct (declared intel_device_info.h:94; only .has_guc = 1
exists, i915_pci.c:469/:520), so intel_guc_ct_disable() can never be
reached (guc_disable_communication only calls it under HAS_GUC_CT,
intel_uc.c:230-231), and enable_guc defaults to 0 anyway
(i915_params.h:49). Additionally the teardown paths that call it
(intel_uc_fini_hw, reset) are privileged driver-lifecycle contexts. The
file is compiled (sys/conf/files:2453) but the function is inert.
PoC status
untested β no runtime reproduction possible: the guest GPU is QEMU bochs
std-VGA (no i915 device), and the code path cannot even be selected on this
tree.
Fix
Quiesce before freeing: disable + synchronize GuC interrupts and switch
guc->send/guc->handler to nop before intel_guc_ct_disable() tears
down the channel (and, defense-in-depth, before ctch_close() inside
intel_guc_ct_disable() itself). See fix.diff (authored against the
read-only sys/ tree; never applied).
DF-4568 β VERDICT
Status: untested Β· impact: none Β· severity Info (hardening) Β· defect confidence: certain
What was verified, and how
Static verdict; the teardown ordering was traced line by line across
intel_uc.c, intel_guc_ct.c, and i915_irq.c.
The race window is real (defect confirmed)
Shutdown path (driver unload / GT reset contexts):
guc_disable_communication() intel_uc.c:226
ββ intel_guc_ct_disable(&guc->ct) intel_uc.c:231 β FIRST
β ββ ctch_close(guc, ctch) intel_guc_ct.c:898
β β ββ guc_action_deregister_ct_buffer Γ2 :284-289
β β ββ ctch_fini β i915_vma_unpin_and_release(&ctch->vma,
β β I915_VMA_RELEASE_MAP) :214-218 β page freed/unmapped
β ββ guc->send/handler = nop :901-902 β AFTER the free
ββ gen9_disable_guc_interrupts(i915) intel_uc.c:233 β masks + synchronize_irq
(i915_irq.c:555-567)
Between the free (intel_guc_ct.c:217) and the IRQ mask+synchronize
(intel_uc.c:233 / i915_irq.c:559-561), an asserted GuC event dispatches
guc->handler, which is still intel_guc_to_host_event_handler_ct
(intel_guc_ct.c:846-851; only replaced at :902, after the free). The handler
runs ct_process_host_channel() (:815-840) against the released mapping:
ctb_read() reads ctb->desc->head/tail (:582-583) and writes
desc->head (:628); the -EPROTO path writes ctb->desc->is_in_error
(:838). UAF read+write in interrupt context. The ctch_is_open() guard
(:143-146, used at :822) is an unlocked check-then-use on ctch->vma, which
i915_vma_unpin_and_release clears non-atomically β it cannot close the
window.
Reachability on this tree: none (verified)
intel_guc_ct_disableis only called underHAS_GUC_CT(i915)(intel_uc.c:230-231) =info.has_guc_ct(i915_drv.h:2665); no platform sets it (declared intel_device_info.h:94; i915_pci.c sets only.has_gucat :469/:520; no runtime assignment anywhere in sys/dev/drm).- Independently,
enable_gucdefaults to 0 (i915_params.h:49), so the GuC is not even loaded; the callers ofguc_disable_communication(intel_uc.c:334, :423, :450 β sanitize/fini/suspend) are privileged driver-lifecycle paths.
Guest PoC feasibility: none
Guest video is QEMU bochs std-VGA (vgapci0@pci0:0:2.0 chip=0x11111234);
no i915 device exists, and the racing path additionally requires the
interrupt-vs-teardown timing plus a loaded GuC β not constructible on this
guest, and not attacker-timed even on real hardware (driver unload/reset
contexts are privileged).
Bottom line
Real lifetime bug in the shutdown ordering (fixed upstream in later i915 by
quiescing interrupts and switching the handler before freeing CT buffers),
but unreachable on this tree and only triggerable from privileged
driver-lifecycle timing even where reachable. Filed as Info hardening.
No PoC run: untested, impact: none, fix_status: not_testable.
fix.diff
Included (authored against read-only sys/, never applied): reorder
guc_disable_communication() to disable+synchronize GuC interrupts and
switch send/handler to nop before intel_guc_ct_disable(), and move the
send/handler switch inside intel_guc_ct_disable() ahead of ctch_close()
as defense in depth.
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_testablenot_testable: the racing path is unreachable on this tree (no has_guc_ct platform, enable_guc default 0) and the guest has no Intel GPU, so neither baseline nor patched behavior can be exercised; the diff is line-accurate and git apply --check passes against the read-only sys/ tree.
findings/poc/DF-4568/fix.diff (git apply --check clean); VERDICT.md section 'fix.diff'.
Confirmed kernel references
- sys/dev/drm/i915/intel_uc.c:226
- sys/dev/drm/i915/intel_uc.c:231
- sys/dev/drm/i915/intel_uc.c:233
- sys/dev/drm/i915/intel_guc_ct.c:887
- sys/dev/drm/i915/intel_guc_ct.c:898
- sys/dev/drm/i915/intel_guc_ct.c:902
- sys/dev/drm/i915/intel_guc_ct.c:214
- sys/dev/drm/i915/intel_guc_ct.c:815
- sys/dev/drm/i915/intel_guc_ct.c:628
- sys/dev/drm/i915/intel_guc_ct.c:838
- sys/dev/drm/i915/i915_irq.c:555
Detail
Evidence (decisive lines)
findings/poc/DF-4568/VERDICT.md (ordering diagram with path:line); decisive lines: intel_uc.c:231 vs :233 (free-before-quiesce), intel_guc_ct.c:898 vs :902 (free-before-handler-swap), intel_guc_ct.c:214-218 (release map), intel_guc_ct.c:628/:838 (writes through desc), i915_irq.c:555-567 (disable+synchronize). fix.diff (2 files) applies clean via git apply --check.
PoC changes
none β no PoC source exists; teardown race on a path unreachable on this tree (no platform sets has_guc_ct; enable_guc default 0) and not attacker-timed even where reachable.
Verified recommended fix
In guc_disable_communication() call gen9_disable_guc_interrupts() and switch guc->send/guc->handler to nop BEFORE intel_guc_ct_disable(); inside intel_guc_ct_disable() swap send/handler before ctch_close() (see fix.diff).
Verdict
Static verdict, defect proven at source level: guc_disable_communication() (intel_uc.c:226-237) calls intel_guc_ct_disable() at :231 β which frees and unmaps the shared CT page via ctch_close (intel_guc_ct.c:898) -> ctch_fini -> i915_vma_unpin_and_release(I915_VMA_RELEASE_MAP) (intel_guc_ct.c:214-218) β BEFORE gen9_disable_guc_interrupts() at intel_uc.c:233 masks+synchronize_irq()s the GuC events (i915_irq.c:555-567), and before guc->handler stops pointing at intel_guc_to_host_event_handler_ct (only switched at intel_guc_ct.c:902, after the free). A GuC event asserted in that window dispatches the CT handler into ct_process_host_channel (intel_guc_ct.c:815-840) which reads and writes through the freed descriptor (desc->head write at :628, is_in_error write at :838) β interrupt-context UAF read+write. The ctch_is_open() guard (:822) is an unlocked check-then-use on ctch->vma and cannot close the window. Unreachable on this tree: no platform ever sets info.has_guc_ct (intel_device_info.h:94 declared; i915_pci.c:469/:520 set only .has_guc), and enable_guc defaults to 0 (i915_params.h:49), so intel_guc_ct_disable can never run here; callers are privileged driver-lifecycle paths anyway. Filed as Info hardening; no runtime PoC possible (guest GPU is QEMU bochs std-VGA, no i915 device, and the window is not attacker-timed).
No comments yet.