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

CTB ring math trusts GuC-firmware-written descriptor fields with only compiled-out GEM_BUG_ON validation β€” desc->size==0 gives divide-by-zero panic, inflated size/tail gives FW-offset wild write past the 1-page CT object

Summary

All ring indices and the free-space check derive from the GuC-DMA-writable shared descriptor page, and every invariant is a GEM_BUG_ON that compiles to a no-op in production: desc->size==0 -> #DE panic; inflated size/tail with arranged head -> cmds[tail] writes at an FW-chosen offset up to ~1GiB past the 4096-byte CT object (heap wild write with partially host-controlled data). Upstream later replaced exactly these checks with real -EPROTO validation. Info because the CT path is doubly unreachable on this tree (no has_guc_ct platform + enable_guc defaults 0). Real-descriptor-validation fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4567 Β· 5 files
FileTypeDescriptionSize
README.md β€” 3.9 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
fix.diff β€” 2.2 KB view raw
verdict.json β€” 4.1 KB view raw
manifest.json β€” 1.4 KB view raw

DF-4567 β€” intel_guc_ct.c: CTB ring math trusts GuC-firmware-written descriptor fields with only compiled-out GEM_BUG_ON validation

  • File: sys/dev/drm/i915/intel_guc_ct.c
  • Severity: Info (hardening / defense-in-depth β€” see reachability)
  • Confidence: certain (the absent validation is statically provable)

What the defect is

ctb_write() (sys/dev/drm/i915/intel_guc_ct.c:317-381) and ctb_read() (:579-630) drive the CT send/receive rings entirely off the shared struct guc_ct_buffer_desc (intel_guc_fwif.h:382-394) that lives in a page also written by the GuC firmware over DMA (desc->head, desc->tail, desc->size, is_in_error, fence, status are all in FW-writable shared memory; the page is mapped WB at intel_guc_ct.c:189-201).

Every bound/invariant on those fields is enforced only by GEM_BUG_ON / GEM_WARN_ON, which are no-ops in production builds (sys/dev/drm/i915/i915_gem.h:56-61 β€” #define GEM_BUG_ON(expr) BUILD_BUG_ON_INVALID(expr) unless CONFIG_DRM_I915_DEBUG_GEM):

  • ctb_write:332-335 β€” GEM_BUG_ON(desc->size % 4), desc->head % 4, desc->tail % 4, tail >= size : all no-ops.
  • ctb_read:590-594 β€” same family plus head >= size: no-ops.

Consequences if the FW ever writes a bad descriptor (GuC bug, FW compromise, or desc corruption from a GT reset β€” the reason upstream i915 later replaced exactly these GEM_BUG_ONs with real error paths):

  1. Divide-by-zero panic: tail = (tail + 1) % size at intel_guc_ct.c:366/369/373 and head = (head + 1) % size at :608/:624 β€” size = desc->size / 4; desc->size == 0 β‡’ % 0 β‡’ #DE β‡’ kernel panic.
  2. Wild OOB write: the ENOSPC space check (:347) and every ring index in ctb_write are computed from FW-written desc->head/tail/size (:324-326). A descriptor with inflated size/tail (and head arranged so used looks small) makes the check pass and cmds[tail] = header (:365-374) land at an FW-chosen offset up to ~1 GiB past the 1-page CT object β€” kernel heap corruption with partially host-controlled data (header dword, fence, action payload).
  3. Space-check bypass / stream corruption on the RECV side for shrunk or wrapped size/tail (mis-counted available, :597-605).

Why it is only Info on this tree (reachability β€” verified)

The entire CT path is dead code in this tree, twice over:

  1. i915_modparams.enable_guc defaults to 0 (i915_params.h:49 param(int, enable_guc, 0)); intel_uc_init_hw() returns early unless USES_GUC() (intel_uc.c:44-48, :342-348), and enable_guc is a root-only loader tunable (mode 0400, i915_params.c:153-156).
  2. Even with enable_guc=1/3, guc_enable_communication() only calls intel_guc_ct_enable() when HAS_GUC_CT(i915) (intel_uc.c:218-219) β€” and no platform in this tree ever sets info.has_guc_ct. The field is declared in the device-info FUNC list (intel_device_info.h:94) but i915_pci.c only ever sets .has_guc = 1 (i915_pci.c:469, :520); rg has_guc_ct across sys/dev/drm finds no other assignment. So intel_guc_ct_enable(), ctb_write(), ctb_read(), intel_guc_send_ct() are never called on this tree, on any hardware.

The file is compiled into the i915 module (sys/conf/files:2453).

PoC status

untested β€” no runtime PoC is possible or meaningful: - the QEMU guest has bochs std-VGA (no Intel dGPU β†’ no i915 GuC at all); - statically, the code path cannot be selected on this tree (see above); - triggering the defect additionally requires the signed GuC firmware to write an out-of-contract descriptor, which no unprivileged input can induce.

Fix

Validate the FW-written descriptor fields for real in ctb_write/ctb_read (replace the no-op GEM_BUG_ONs with if (...) return -EPROTO;), as upstream i915 did when it hardened the CT code. See fix.diff in this directory (authored against the read-only sys/ tree; never applied).

VERDICT.md
↓ download raw

DF-4567 β€” VERDICT

Status: untested Β· impact: none Β· severity Info (hardening) Β· defect confidence: certain

What was verified, and how

This is a static verdict. The finding was proven at the source level, line by line; no dynamic reproduction exists, and none is possible on this audit guest. Three independent claims were verified:

1. The descriptor trust is real (defect confirmed)

ctb_write (sys/dev/drm/i915/intel_guc_ct.c:317) and ctb_read (intel_guc_ct.c:579) take head, tail, size straight from ctb->desc (intel_guc_ct.c:324-326, :582-584) β€” the shared guc_ct_buffer_desc (intel_guc_fwif.h:382-394) that the GuC firmware updates by DMA ("head: offset updated by GuC", "tail: offset updated by owner" β€” both live in the same WB-mapped page pinned at intel_guc_ct.c:189). The only validation is:

GEM_BUG_ON(desc->size % 4);   /* intel_guc_ct.c:332-335, 590-594 */
GEM_BUG_ON(desc->head % 4);
GEM_BUG_ON(desc->tail % 4);
GEM_BUG_ON(tail >= size);

and on this tree GEM_BUG_ON compiles to BUILD_BUG_ON_INVALID(expr) (sys/dev/drm/i915/i915_gem.h:60) unless CONFIG_DRM_I915_DEBUG_GEM (i915_gem.h:38) β€” i.e. nothing is checked in the production kernel. Arithmetic consequences:

  • desc->size == 0 (FW write) β‡’ size = 0 β‡’ % size at intel_guc_ct.c:366/369/373 (ctb_write) and :608/:624 (ctb_read) β‡’ integer divide-by-zero β‡’ kernel panic.
  • Inflated desc->size/desc->tail with desc->head chosen so used + len + 1 < size passes at intel_guc_ct.c:347 β‡’ cmds[tail] writes at intel_guc_ct.c:365-374 execute with an FW-chosen index (up to size-1 dwords past cmds, size up to 0x3FFFFFFF) β‡’ wild kernel-memory write past the 1-page (4096-byte) CT object (layout at intel_guc_ct.c:158-178). Data written is partially host-controlled (header/fence/action payload).
  • Shrunk/wrapped values on the RECV side mis-derive available (intel_guc_ct.c:597-605) β€” stream confusion, bounded reads (indexes are masked by % size), plus the same % 0 panic.

2. Reachability on this tree: none (verified twice over)

  • enable_guc default 0: sys/dev/drm/i915/i915_params.h:49 (param(int, enable_guc, 0)); help text "0=disable [default]" (i915_params.c:153-156). intel_uc_init_hw bails unless USES_GUC() (intel_uc.c:342-348; predicate intel_uc.h:44-48).
  • Even with GuC enabled, CT is only selected by HAS_GUC_CT(i915) (intel_uc.c:218-219) = info.has_guc_ct (i915_drv.h:2665). That flag is declared in intel_device_info.h:94 and set by no platform: grep across sys/dev/drm finds only the declaration, the accessor, and .has_guc = 1 in i915_pci.c:469/:520 (GEN9_FEATURES, i915_pci.c:463-474). So intel_guc_ct_enable() (intel_guc_ct.c:861) is unreachable code on this tree; MMIO mode is always chosen.

The file is compiled (sys/conf/files:2453) but inert.

3. Guest PoC feasibility: none

Guest video hardware is QEMU bochs std-VGA (vgapci0@pci0:0:2.0: chip=0x11111234), so no i915 device binds at all; and triggering the defect requires the signed GuC firmware to write an out-of-contract descriptor β€” not inducible by unprivileged guest input even on real Intel hardware.

Bottom line

Real robustness/hardening defect (the exact class upstream i915 later fixed by replacing these GEM_BUG_ONs with -EPROTO validation), but on this tree it is doubly unreachable (no platform sets has_guc_ct; enable_guc defaults 0, root-only) and requires GuC-firmware misbehavior. Filed as Info hardening for upstream contribution. No PoC run: untested, impact: none, fix_status: not_testable.

fix.diff

Included (authored against read-only sys/, never applied): converts the ctb_write/ctb_read GEM_BUG_ONs into real descriptor validation returning -EPROTO before any divide or index write.

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 vulnerable path is unreachable on this tree (no platform sets has_guc_ct; enable_guc default 0) and the guest has no Intel GPU, so baseline bad behavior cannot be produced to compare against a patched kernel; the diff is line-accurate against the read-only sys/ tree and git apply --check passes.

findings/poc/DF-4567/fix.diff (git apply --check clean); VERDICT.md section 'fix.diff'.
↓ fix.diffper-fix-DF-4567

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4567/VERDICT.md (full line-cited narrative); decisive lines: intel_guc_ct.c:324-335 (unchecked desc math + no-op GEM_BUG_ON), :347 (ENOSPC check on FW data), :365-373 (cmds[tail] write + % size divide), :582-594, :608/:624; i915_gem.h:60 (GEM_BUG_ON no-op); i915_pci.c:469 (has_guc only); intel_uc.c:218-223; i915_params.h:49. fix.diff applies clean (git apply --check verified).

PoC changes

none β€” no PoC source exists; the finding is a hardening defect on a path that cannot be selected on this tree (no platform sets has_guc_ct; enable_guc defaults 0). Runtime trigger additionally requires the signed GuC firmware to write an out-of-contract descriptor.

Verified recommended fix

Replace the compiled-out GEM_BUG_ON descriptor checks in ctb_write/ctb_read with real validation of desc->size (== PAGE_SIZE/4 as allocated), desc->head/tail alignment and < size, returning -EPROTO before any divide or ring write (see fix.diff).

Verdict

Static verdict, defect proven at source level: ctb_write/ctb_read (sys/dev/drm/i915/intel_guc_ct.c:317-381,579-630) perform all ring arithmetic on GuC-firmware-writable descriptor fields (desc->size/head/tail, intel_guc_fwif.h:382-394) validated only by GEM_BUG_ON, which compiles to a no-op in production (i915_gem.h:56-61). desc->size==0 gives a divide-by-zero panic at intel_guc_ct.c:366/369/373 and :608/:624; inflated size/tail with suitable head passes the ENOSPC check (:347) and turns cmds[tail] writes (:365-374) into a wild kernel write up to ~1GiB past the 1-page CT object. However the whole CT path is unreachable on this tree twice over: no platform ever sets info.has_guc_ct (declared intel_device_info.h:94; i915_pci.c only sets .has_guc at :469/:520) so guc_enable_communication always picks MMIO mode (intel_uc.c:218-223), and enable_guc defaults to 0 as a root-only tunable (i915_params.h:49). Filed as Info hardening; no runtime PoC exists or is possible (guest has bochs std-VGA, no i915 device, and triggering requires signed-GuC-firmware misbehavior).