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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4568 Β· 5 files
FileTypeDescriptionSize
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 with intel_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:

  1. guc_disable_communication() (sys/dev/drm/i915/intel_uc.c:226-237) calls intel_guc_ct_disable(&guc->ct) at intel_uc.c:231 β€” before gen9_disable_guc_interrupts(i915) at intel_uc.c:233 (which is the call that masks the GuC events and synchronize_irq()s them, i915_irq.c:555-567).
  2. Inside intel_guc_ct_disable() (intel_guc_ct.c:887-904), the shared CT page is deregistered and freed/unmapped via ctch_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, are guc->send/guc->handler switched to the nop versions.
  3. A GuC event that is already asserted (or arrives) in that window dispatches guc->handler β€” still intel_guc_to_host_event_handler_ct (intel_guc_ct.c:846-851) β€” which runs ct_process_host_channel() (:815-840) on the freed page: ctb_read() dereferences ctb->desc (:581) and writes desc->head = head * 4 (:628); on -EPROTO it also writes ctb->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).

VERDICT.md
↓ download raw

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_disable is only called under HAS_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_guc at :469/:520; no runtime assignment anywhere in sys/dev/drm).
  • Independently, enable_guc defaults to 0 (i915_params.h:49), so the GuC is not even loaded; the callers of guc_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_testable
baseline no→ patch + rebuild →patched clean

not_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'.
↓ fix.diffper-fix-DF-4568

Confirmed kernel references

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).