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)
PoC verification
Evidence pack
findings/poc/DF-5220 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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).
- Gen8+ (HAS_LOGICAL_RING_CONTEXTS) engine bring-up:
intel_engines_init()(intel_engine_cs.c:383) runsclass_info->init_execlists(engine)=logical_render_ring_init()/logical_xcs_ring_init()(intel_lrc.c:2476/2515). - 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.) - 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 atintel_engine_init_breadcrumbs()(intel_engine_cs.c:628) orinit_status_page()/init_phys_status_page()(intel_engine_cs.c:633- 638) with an allocation-class error. - 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 througherr: intel_engine_cleanup_common(engine)at intel_ringbuffer.c:1480-1483. 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 whileengine->timeline.linkis STILL ONgt.timelines, and while the kernel-context ring pinned in step 3 is still pinned (no__intel_context_unpinruns for it).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_delwritesprev->next/next->prevthrough 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).- 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=0xffffffff82600540appears in a zeroed, reallocated chunk afterkfree(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_testableFix 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
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.
No comments yet.