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

Use-after-free: execlists engine-init failure kfree()s the engine while engine->timeline.link is still on gt.timelines; the unwind's neighbor list_del writes kernel pointers into the freed chunk

Summary

Gen8+ execlists ring_init failure returns without intel_engine_cleanup_common (unlike the legacy err label at intel_ringbuffer.c:1480-83) - intel_engines_init then kfrees the engine while its embedded timeline link is live on gt.timelines; i915_gem_contexts_fini's timeline fini list_del (:75) then writes kernel pointers through the freed slab-LIFO-reused chunk (x2 8-byte writes at fixed offsets) + dormant stale node for every later gt.timelines walk. Allocation-failure-timed (boot/module-load) so Medium not High. Mechanism proven deterministically 3/3 on guest with standalone KLD (repro_kld.c, pointer write into reclaimed chunk at offset 72); driver path itself needs Intel GPU. err-label mirror fix verified APPLIES_CLEAN.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5220 Β· 11 files
FileTypeDescriptionSize
repro_kld.c β€” 3.7 KB view raw
build.sh β€” 188 B view raw
run.sh β€” 143 B view raw
build.log β€” 5.5 KB view raw
run.log β€” 1.7 KB view raw
run.2.log β€” 444 B view raw
run.3.log β€” 444 B view raw
env.txt β€” 817 B view raw
VERDICT.md β€” 5.9 KB ↓ raw
README.md β€” 1.8 KB ↓ raw
fix.diff β€” 910 B view raw

DF-5220 β€” i915 execlists engine-init error path kfree()s the engine while engine->timeline.link is still on i915->gt.timelines (use-after-free class)

PoC: repro_kld.c β€” a DragonFly KLD that reproduces the corruption MECHANISM (neighbor intrusive-list unlink writing a kernel pointer into a freed-and-reallocated slab chunk) without any GPU. The full driver path cannot execute on this guest (QEMU virtual VGA 0x1234:0x1111; i915 can never attach) β€” see VERDICT.md for the line-accurate static proof.

BUILD (on the DragonFly guest, needs /usr/src/sys):

mkdir -p /tmp/df5220 && cd /tmp/df5220
cp <pack>/repro_kld.c df5220.c
printf "KMOD=   df5220\nSRCS=   df5220.c\n\n.include <bsd.kmod.mk>\n" > Makefile
make

RUN (as root):

kldload ./df5220.ko
dmesg | grep df5220
kldunload df5220

EXPECTED OUTPUT (observed 3/3 runs, identical):

df5220[0]: A=0xfffff8008d520a80 C=0xfffff8008d520b10 A.le_prev=0xfffff8008d520b50 C.le_next=0xfffff8008d520a80
df5220[0]: D=0xfffff8008d520a80 (reclaim of A: HIT)
df5220[0]: after LIST_REMOVE(C): D.le_next=0 D.le_prev=0xffffffff82600540
df5220[0]: VULN-WRITE PROVEN: pointer 0xffffffff82600540 written into reallocated chunk D at offset 72 after kfree(A)
df5220: mechanism demonstrated: neighbor unlink writes into freed-and-reallocated memory

Success criterion: after kfree(A) (while still linked) and LIFO reclaim of A's chunk by a zeroed allocation D, unlinking the NEIGHBOR C leaves a non-NULL kernel pointer at D+72 β€” an 8-byte write into reallocated freed memory. This is exactly the write performed by i915_timeline_fini()'s list_del() (sys/dev/drm/i915/i915_timeline.c:75) through the freed engine node during the i915_gem_init error unwind.

Guest stays healthy; module unloads cleanly.

VERDICT.md
↓ download raw

DF-5220 VERDICT β€” execlists engine-init failure kfree()s engine with live timeline list node

Bottom line

The defect is proven statically, line by line; the driver-level trigger cannot execute on this guest because it has no Intel graphics device (QEMU virtual VGA, chip 0x1234:0x1111 β€” pciconf -lv in env.txt), so the finding itself is INCONCLUSIVE at runtime: not reproduced end-to-end, not refuted. The corruption mechanism the defect depends on was reproduced deterministically (3/3 runs) with a standalone KLD (repro_kld.c, run.log / run.2.log / run.3.log): an intrusive-list unlink of a neighbor writes an 8-byte kernel pointer (0xffffffff82600540) into a freed-and-then-reallocated slab chunk, at the exact offset of the dead node's back-pointer. The guest remained healthy and the module unloaded cleanly after every run.

The defect (static trace)

Protocol: i915_timeline_init() links every timeline β€” embedded engine timelines included β€” onto the global i915->gt.timelines list (sys/dev/drm/i915/i915_timeline.c:28 list_add(&timeline->link, &i915->gt.timelines)). The ONLY sanctioned unlink is i915_timeline_fini() β†’ list_del(&timeline->link) (i915_timeline.c:75), reached for embedded engine timelines exclusively via intel_engine_cleanup_common() (sys/dev/drm/i915/intel_engine_cs.c:680).

  1. Gen8+ (HAS_LOGICAL_RING_CONTEXTS) engine bring-up: intel_engines_init() (intel_engine_cs.c:383) runs class_info->init_execlists(engine) = logical_render_ring_init() / logical_xcs_ring_init() (intel_lrc.c:2476/2515).
  2. Those call logical_ring_setup() (intel_lrc.c:2404) β†’ intel_engine_setup_common() (intel_engine_cs.c:482) β†’ i915_timeline_init(..., &engine->timeline, ...) β€” the embedded timeline is now linked onto gt.timelines. (engines are kzalloc'd at intel_engine_cs.c:286.)
  3. Then logical_ring_init() (intel_lrc.c:2424) β†’ intel_engine_init_common() (intel_engine_cs.c:602) which pins the kernel context (creating its per-engine ring + a HEAP timeline, also linked onto gt.timelines, intel_ringbuffer.c:1190 / execlists_context_deferred_alloc intel_lrc.c:2848) and can then FAIL at intel_engine_init_breadcrumbs() (intel_engine_cs.c:628) or init_status_page()/init_phys_status_page() (intel_engine_cs.c:633- 638) with an allocation-class error.
  4. The execlists error path returns WITHOUT any unwind (intel_lrc.c:2430-2431 if (ret) return ret;, and wrappers return it verbatim). Contrast the legacy ring path which self-unwinds through err: intel_engine_cleanup_common(engine) at intel_ringbuffer.c:1480-1483.
  5. intel_engines_init()'s cleanup loop then does, for the failing engine (id == err_id): kfree(engine) (intel_engine_cs.c:417) β€” the engine is freed while engine->timeline.link is STILL ON gt.timelines, and while the kernel-context ring pinned in step 3 is still pinned (no __intel_context_unpin runs for it).
  6. i915_gem_init() fails β†’ goto err_context (i915_gem.c:6013-6017) β†’ i915_gem_contexts_fini() (i915_gem_context.c:593) β†’ destroy_kernel_context() β†’ ring teardown β†’ intel_ring_free() (intel_ringbuffer.c:1213-1223) β†’ i915_timeline_put() β†’ __i915_timeline_free() (i915_timeline.c:93-100) β†’ i915_timeline_fini() β†’ list_del(&timeline->link) on a node whose neighbor is the FREED engine node β†’ __list_del writes prev->next/next->prev through the freed engine's memory (use-after-free WRITE; 2Γ— 8-byte kernel-pointer writes at fixed offsets in a freed, likely-reallocated slab chunk).
  7. Secondary: the stale node itself remains on gt.timelines (dormant use-after-free for any later walk β€” i915_timelines_park i915_timeline.c:52-67, i915_gem.c:3775/4209, i915_request.c:240 β€” and the WARN at i915_gem.c:6302 on unload).

This mirrors the mechanism demonstrated by the KLD: kfree-while-linked, then a neighbor's unlink writes into the freed chunk β€” deterministic (slab LIFO) observed 3/3 times.

Trigger & reachability (honest)

  • Requires: Intel Gen8+ graphics (execlists) AND an allocation failure at the breadcrumbs / status-page stage of engine bring-up (kernel-memory exhaustion at driver attach: boot or kldload of the drm modules).
  • i915 attach happens at boot / module load; an unprivileged user cannot time it. Root can retry kldload/kldunload under memory pressure.
  • On this guest: NO Intel GPU exists β†’ driver path cannot run at all (env.txt: pciconf shows vgapci0 chip=0x11111234, virtual VGA).
  • Therefore: severity capped at Medium (kernel UAF write class, but environmental/privileged trigger).

Runtime evidence

  • build.log β€” clean KLD build (cc 8.3, -Werror, no warnings).
  • run.log, run.2.log, run.3.log β€” three identical successful demonstrations; D.le_prev=0xffffffff82600540 appears in a zeroed, reallocated chunk after kfree(A) + LIST_REMOVE(C).
  • env.txt β€” guest uname, compiler, PCI display device, kldstat, dmesg (no i915/drm).

Fix validation

Not possible on this guest (baseline cannot run β€” no Intel GPU). fix.diff was authored from the line-accurate trace: it makes the execlists wrappers unwind intel_engine_setup_common() on logical_ring_init() failure via intel_engine_cleanup_common(), exactly matching the legacy path's existing error contract (intel_ringbuffer.c:1480-1483). fix_status: not_testable.

Class / bucket

memcorrupt (UAF write). The DF-5219 companion finding (same audit, timeline kref leak in intel_engine_create_ring's error path) shares the i915_timeline_get/put protocol surface.

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

Fix validation impossible on this guest: the baseline driver path requires Intel Gen8+ graphics hardware (absent, env.txt). fix.diff was authored from the line-accurate static trace and verified to apply cleanly against the read-only sys/ tree (git apply --check: APPLIES_CLEAN); it restores exact parity with the legacy init path's error contract

findings/poc/DF-5220/fix.diff (git-apply-able, verified APPLIES_CLEAN); VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-5220

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log line 3-4 ('VULN-WRITE PROVEN: pointer 0xffffffff82600540 written into reallocated chunk D at offset 72 after kfree(A)'), identical in run.2.log/run.3.log; VERDICT.md static trace steps 1-7 citing intel_lrc.c:2430-2431, intel_engine_cs.c:417, i915_timeline.c:28+75; env.txt proving no Intel GPU on guest

PoC changes

No seed existed; repro_kld.c was written from scratch as a GPU-free mechanism demo (kfree-while-linked + LIFO reclaim + neighbor LIST_REMOVE), after establishing that the driver path cannot run on a guest with no Intel graphics device

Verified recommended fix

Unwind intel_engine_setup_common() on logical_ring_init() failure: call intel_engine_cleanup_common(engine) in the error paths of logical_render_ring_init/logical_xcs_ring_init (mirrors the legacy path's existing err label at intel_ringbuffer.c:1480-1483); see fix.diff

Verdict

Defect proven statically line-by-line; driver-level reproduction impossible on this guest (QEMU virtual VGA 0x1234:0x1111, no Intel graphics, i915 can never attach β€” env.txt). The corruption mechanism the defect depends on was reproduced deterministically 3/3 runs with a standalone KLD: kfree() of an object still linked into an intrusive list, slab-LIFO reclaim of the chunk, then a NEIGHBOR's list unlink writing kernel pointer 0xffffffff82600540 into the reallocated chunk at offset 72 (run.log/run.2.log/run.3.log). In the driver, that neighbor-unlink is i915_timeline_fini()'s list_del (i915_timeline.c:75) executed on the kernel-context ring timeline during i915_gem_contexts_fini(), after intel_engines_init()'s cleanup kfree()d the failing engine (intel_engine_cs.c:417) without unwinding logical_ring_setup()'s timeline link (intel_lrc.c:2404-2431 return error with no cleanup; legacy path self-unwinds at intel_ringbuffer.c:1480-1483). Net: 2x 8-byte kernel-pointer writes into a freed/reused slab chunk at fixed offsets + a dormant stale node on gt.timelines. Trigger: allocation failure at engine bring-up (breadcrumbs/status-page stage) on Gen8+ execlists hardware at driver attach β€” boot or module load β€” so not unprivileged-timed; severity capped Medium.