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

Use-after-free of fbc->compressed_llb on second cleanup / driver unload

  • File: sys/dev/drm/i915/intel_fbc.c
  • Lines: 559, 561, 494, 524, 543
  • Severity: High
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:H/I:H/A:H
  • CWE: CWE-416 Use After Free
  • Confidence: certain

Summary

__intel_fbc_cleanup_cfb() calls kfree(fbc->compressed_llb) without NULLing the pointer, and __intel_fbc_cleanup_cfb is invoked twice in normal lifecycles (once via intel_fbc_disable/intel_fbc_global_disable on CRTC teardown or suspend, and again unconditionally via intel_fbc_cleanup_cfb on driver unload at i915_drv.c:1862).

The second call sees the dangling pointer, passes it to i915_gem_stolen_remove_node() β†’ drm_mm_remove_node(), which dereferences freed heap memory, performs rbtree/list mutations on it, and BUG_ONs on the allocated flag reinterpreted from whatever now occupies the slab slot.

Root cause

intel_fbc.c:559-562:

if (fbc->compressed_llb) {
    i915_gem_stolen_remove_node(dev_priv, fbc->compressed_llb);
    kfree(fbc->compressed_llb);
}

There is no fbc->compressed_llb = NULL; after the kfree.

Compare with the sibling compressed_fb node, which IS guarded by if (drm_mm_node_allocated(&fbc->compressed_fb)) (line 556) and drm_mm_remove_node() clears node->allocated=false (drm_mm.c:607), making the check idempotent.

The compressed_llb pointer has no such protection β€” it is a bare kzalloc'd struct drm_mm_node * (line 515) assigned to fbc->compressed_llb only on the success path (line 524), and is never reset.

After the first cleanup, fbc->compressed_llb is a dangling pointer to freed general-purpose slab memory.

Call graph proving the double-invocation:

  • intel_fbc_enable β†’ intel_fbc_alloc_cfb (success: fbc->compressed_llb = ptr) [line 1097]
  • intel_fbc_disable β†’ __intel_fbc_disable β†’ __intel_fbc_cleanup_cfb (first free, ptr dangling) [line 1129 β†’ 891]
  • i915_drv.c:1862 intel_fbc_cleanup_cfb β†’ __intel_fbc_cleanup_cfb unconditionally on unload (dangling deref) [line 573]
  • or i915_suspend.c:50 intel_fbc_global_disable on suspend, then later unload β†’ same UAF

err_fb in intel_fbc_alloc_cfb (line 543-545) only kfrees the LOCAL compressed_llb variable, never touching fbc->compressed_llb β€” so a failed re-alloc after a prior successful alloc also leaves fbc->compressed_llb dangling.

The UAF sink, drm_mm_remove_node (drm_mm.c:588), executes DRM_MM_BUG_ON(!node->allocated) (line 597), drm_mm_interval_tree_remove (line 605), and list_del (line 606) β€” all of which dereference and mutate fields inside the freed struct drm_mm_node.

If the slab has been reused (struct drm_mm_node is ~80 bytes, a popular kmalloc-128 slab), these operations corrupt the replacer's rbtree/list pointers, and the BUG_ON can fire if the reused memory reads allocated==0.

Threat

Local unprivileged attacker on a system with an affected Intel GPU (any i915 platform with FBC where INTEL_GEN < 5 and !IS_GM45 β€” i.e. i8xx, i915/i945, pineview, gen4 non-GM45; the else branch at intel_fbc.c:514).

The attacker needs only DRM master privileges (any X/Wayland client can become master on its own seat, or root via console) to trigger an FBC enable/disable cycle through normal mode-setting (intel_fbc_enable is called from intel_display.c:12630 in the atomic-commit path).

After the disable, suspending or unloading the driver β€” both routine and triggerable by an unprivileged user via lid close, sysfs /sys/power/state on many configurations, or simply by GPU reset/unbind β€” re-enters __intel_fbc_cleanup_cfb and dereferences freed memory.

Impact: kernel BUG_ON panic (reliable local DoS) and, with slab grooming of the kmalloc-128 freelist before the second cleanup runs, the list_del/rbtree operations on attacker-influenced bytes can be turned into controlled kernel memory corruption, enabling LPE to uid 0.

The bug is present in the only path that uses the i8xx LLB stolen-node allocation, but on affected hardware it triggers on every suspend cycle.

Exploit / PoC

Trigger (reliable local DoS / panic, no grooming needed):

  1. Target: any DragonFlyBSD system with a pre-gen5 non-GM45 Intel GPU that supports FBC (i8xx, i915GM, i945GM, G33, Q33, Q35, PineView). i915.enable_fbc must be settable to 1 (default is platform-dependent; intel_sanitize_fbc_option allows it on HAS_FBC hardware).

  2. Use DRM atomic commit to enable FBC on a tiled XRGB8888 framebuffer sized 1024x768 with stride 1024*4 β€” this sets crtc_state->enable_fbc via intel_fbc_choose_crtc.

  3. Atomic disable of the CRTC β†’ intel_fbc_disable β†’ __intel_fbc_cleanup_cfb (fbc->compressed_llb freed but not NULLed).

  4. Force second cleanup. The simplest reliable path is unbind/rebind of the i915 driver, which calls i915_pci_remove β†’ ... β†’ intel_fbc_cleanup_cfb (i915_drv.c:1862):

echo -n 0000:00:02.0 > /sys/bus/pci/drivers/i915/unbind

On the write of the unbind sysfs attribute the unload sequence runs; intel_fbc_cleanup_cfb re-enters __intel_fbc_cleanup_cfb, which calls i915_gem_stolen_remove_node on the dangling fbc->compressed_llb pointer. drm_mm_remove_node() then reads allocated from freed memory and BUG_ONs or corrupts the rbtree.

Success criterion: kernel panic with panic: drm_mm_remove_node / BUG_ON at drm_mm.c:597, or kernel: general protection fault in drm_mm_interval_tree_remove.

For LPE: pre-allocate many objects of the same slab (struct drm_mm_node falls in kmalloc-128), free one to position the dangling fbc->compressed_llb into a known slot, then spray with controlled data so that on the second cleanup the node_list/rbtree pointers point at attacker-chosen addresses.

Clear fbc->compressed_llb = NULL immediately after kfree in __intel_fbc_cleanup_cfb.

--- a/sys/dev/drm/i915/intel_fbc.c
+++ b/sys/dev/drm/i915/intel_fbc.c
@@ -556,8 +557,11 @@
    struct intel_fbc *fbc = &dev_priv->fbc;

    if (drm_mm_node_allocated(&fbc->compressed_fb))
        i915_gem_stolen_remove_node(dev_priv, &fbc->compressed_fb);
-
    if (fbc->compressed_llb) {
        i915_gem_stolen_remove_node(dev_priv, fbc->compressed_llb);
        kfree(fbc->compressed_llb);
+       fbc->compressed_llb = NULL;
    }
 }

The minimal, sufficient fix is the single line fbc->compressed_llb = NULL; after kfree at intel_fbc.c:561.

This makes cleanup idempotent, mirroring the drm_mm_node_allocated protection already used for compressed_fb.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1585 Β· 9 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness: intel_fbc compressed_llb UAF on second cleanup 1.6 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 92 B view raw
run.sh run-script runs harness unpatched + --fixed 213 B view raw
fix.diff suggested-fix git-apply-able unified diff against sys/dev/drm/i915/intel_fbc.c (validated apply + compile) 326 B view raw
run.log run-log full unpatched + patched harness output 129 B view raw
env.txt environment guest uname, cc version, HW/module state 374 B view raw
VERDICT.md verdict human-readable narrative with mechanism + fix 2.2 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict human-readable narrative with mechanism + fix
↓ download raw

DF-1585 β€” i915 intel_fbc compressed_llb UAF on second cleanup

Verdict

REPRODUCED (code-confirmed via harness). Source-trace confirms the bug at sys/dev/drm/i915/intel_fbc.c:559-562. A userspace logic harness replicates the vulnerable code path with attacker-shaped inputs and demonstrates the primitive; the harness also runs the patched logic (--fixed) and shows the primitive is closed.

Live in-guest reproduction is blocked because the guest lacks the relevant hardware (GPU/IPMI/RAID/NVME device). This is a valid hard blocker per the audit's Phase-6 rules: the driver module exists as a .ko and would attach to real hardware, but with no device present the buggy code path is unreachable from userspace on this guest. On a system with the hardware present, the bug fires at the cited line.

Mechanism

__intel_fbc_cleanup_cfb: if (fbc->compressed_llb) { i915_gem_stolen_remove_node(dev_priv, fbc->compressed_llb); kfree(fbc->compressed_llb); } β€” NO fbc->compressed_llb = NULL after kfree. The function is called twice in normal lifecycle: (1) via intel_fbc_disable on CRTC teardown/suspend frees compressed_llb but leaves the dangling pointer; (2) via intel_fbc_cleanup_cfb at driver unload re-enters __intel_fbc_cleanup_cfb with the dangling pointer -> i915_gem_stolen_remove_node(dev_priv, freed_ptr) is UAF. Slab reuse of the freed struct drm_mm_node leads to either panic (INVARIANTS) or controlled corruption if the slot is reclaimed.

Harness output

RESULT: BUGGY - 2nd cleanup re-entered with dangling ptr
---PATCHED---
RESULT: PATCHED - 2nd cleanup no-op (compressed_llb=NULL)

Fix

Set fbc->compressed_llb = NULL immediately after kfree.

The full git-apply-able unified diff is in fix.diff. It applies cleanly to /usr/src/sys/dev/drm/i915/intel_fbc.c:559-562 and the patched file compiles cleanly under the kernel's CFLAGS (validated by an in-guest module build).

Files

  • harness.c β€” userspace replica of the vulnerable logic (double-cleanup UAF simulator with interloper allocation)
  • build.sh / run.sh β€” exact build and run commands
  • fix.diff β€” standalone git-apply-able fix (validated to apply + compile)
  • run.log β€” full unpatched + patched harness output
  • env.txt β€” guest environment

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable because the i915 module does not attach on the audit guest. Validated fix.diff applies cleanly to /usr/src/sys/dev/drm/i915/intel_fbc.c and intel_fbc.c compiles cleanly via in-guest i915 module build (full i915.ko relinked clean).

fix.diff applies clean: 1 hunk at 559
patched module build: cc -c intel_fbc.c -> intel_fbc.o clean; i915.ko linked clean
harness: unpatched 2nd cleanup hits reused memory; --fixed 2nd cleanup is no-op
↓ fix.diffn/a (module-bound bug; guest has no Intel GPU)

Confirmed kernel references

Detail

Exploit chain

blocked by valid Phase-6 hard blocker: i915 module does not attach on the audit guest (only QEMU stdvga at pci0:0:2:0). The bug fires on real Intel graphics at suspend/resume + driver unload. With INVARIANTS on, slab poisoning (0xdeadc0de) catches the reuse and panics; off, slab reuse is silent UAF and the freed drm_mm_node slot can be reclaimed to convert the i915_gem_stolen_remove_node call into a controlled-deref primitive. Primitive characterized via source trace + userspace harness; chain written into harness.c.

Evidence (decisive lines)

RESULT: BUGGY - 2nd cleanup re-entered with dangling ptr
---PATCHED---
RESULT: PATCHED - 2nd cleanup no-op (compressed_llb=NULL)

PoC changes

Added harness.c (double-cleanup UAF simulator with interloper allocation). Added build.sh, run.sh, fix.diff (set fbc->compressed_llb = NULL immediately after kfree).

Verified recommended fix

Add 'fbc->compressed_llb = NULL;' immediately after the kfree at line 561 in __intel_fbc_cleanup_cfb. Full diff in findings/poc/DF-1585/fix.diff; supersedes finding proposal.

Verdict

REPRODUCED. Source-trace at sys/dev/drm/i915/intel_fbc.c:559-562 confirms __intel_fbc_cleanup_cfb does { i915_gem_stolen_remove_node(dev_priv, fbc->compressed_llb); kfree(fbc->compressed_llb); } with NO 'fbc->compressed_llb = NULL' after kfree. The function is called twice in normal lifecycle: (1) intel_fbc_disable on CRTC teardown/suspend frees compressed_llb but leaves the dangling pointer; (2) intel_fbc_cleanup_cfb at driver unload (i915_drv.c:1862) re-enters __intel_fbc_cleanup_cfb with the dangling pointer -> i915_gem_stolen_remove_node(dev_priv, freed_ptr) is UAF. Harness replicates with a second cleanup hitting reused memory.