i915_gem_context unlocked handle lookups race idr_grow's synchronous kfree of the IDR node array (DragonFly shim has no RCU) -> use-after-free read returns attacker-groomed contents as struct i915_gem_context *
Summary
destroy/getparam/setparam/reset_stats resolve the user ctx_id with lookup()/lookup_rcu() outside any lock - safe on Linux because IDR reads are RCU-protected, but on DF rcu_read_lock() is a no-op fence and idr_find takes no token, while idr_grow (reached from this file's create path past 255 contexts) swaps in a new node array and kfrees the old one synchronously. A lookup that loads idr_nodes before the swap dereferences freed memory; groomed contents return as the context pointer feeding kref_get_unless_zero (refcount write at controlled offset) and context_close's set_bit/list_del_init/lut_close pointer-driven writes. VERIFIED on guest via KLD driving the exact primitives: canary pointer captured 20/20 and 9/10 rounds amplified; unamplified 500-round stress PANICKED in idr_find+0x48; fix (defer the kfree ~1s via callout, mirroring the shim's kfree_rcu convention) yields 0 captures and no panic across 1,598,684 native finds. One ordinary create racing one ordinary lookup on a shared render-node fd suffices - no double-free, no i915 logic bug. Shim-wide root cause in sys/libkern/linux_idr.c (systemic class with DF-4329/4197); i915 is the demonstrated consumer. Full uid=0 chain analyzed but not executed (no Intel GPU on guest).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4506 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| idr_race.c | β | 6.5 KB | view raw | |
| Makefile | β | 52 B | β download | |
| amplifier.diff | β | 2.1 KB | view raw | |
| fix.diff | β | 3.0 KB | view raw | |
| build.sh | β | 270 B | view raw | |
| run.sh | β | 395 B | view raw | |
| build.log | β | 1.3 KB | view raw | |
| run.log | β | 2.1 KB | view raw | |
| run.fixed.log | β | 1.2 KB | view raw | |
| panic.txt | β | 1.8 KB | view raw | |
| env.txt | β | 435 B | view raw | |
| verdict.json | β | 5.2 KB | view raw | |
| manifest.json | β | 1.6 KB | view raw |
DF-4506 β i915_gem_context unlocked IDR lookups vs idr_grow kfree (UAF read)
What this pack proves
sys/dev/drm/i915/i915_gem_context.c resolves user context handles with
no lock at all:
- destroy ioctl:
i915_gem_context_lookup()ati915_gem_context.c:831(struct_mutex only acquired afterwards at :835) - getparam
:855, setparam:901(never take struct_mutex) - reset_stats
:981(__i915_gem_context_lookup_rcuunder barercu_read_lock())
On Linux this is safe because IDR reads are RCU-protected. On DragonFly:
rcu_read_lock()iscpu_ccfence()β a no-op with no grace period (sys/dev/drm/include/linux/rcupdate.h:46-50,sys/dev/drm/include/linux/preempt.h:33)idr_find()takes no token (sys/libkern/linux_idr.c:515-528)idr_grow()β reached from the same file's create path (i915_gem_context.c:355idr_allocwhen the 255-entry array is full) β swaps in a new node array and kfrees the old one synchronously (sys/libkern/linux_idr.c:449-459)
=> a lockless idr_find() that loaded idp->idr_nodes before the swap
dereferences freed memory after the swap. If the freed 4080-byte array is
re-groomed with controlled contents, idr_find hands the ioctl an
attacker-crafted struct i915_gem_context *, which flows into
kref_get_unless_zero (refcount write), and on the destroy path into
context_close() β set_bit on ctx->flags, list_del_init on
ctx->hw_id_link (unlink writes), lut_close() radix/list walks β i.e.
fully pointer-driven kernel write primitives.
The i915 ioctl surface itself needs Intel GPU hardware (the audit guest has only virtio-vga), so the PoC reproduces the vulnerable pattern directly against the same shim primitives the ioctls use.
Files
idr_race.c+Makefileβ KLD reproducer (producer = fill+grow+canary, consumer = locklessidr_find, detector for the 0xAA canary pointer)amplifier.diffβ guest-only instrumentation: sysctl-gated tsleep betweenidr_find's node-pointer load and its dereference. Enables an interleaving the uninstrumented code already permits (loads are unordered plain C); default off.fix.diffβ the real fix: defer the kfree of old/destroyed node arrays by 1s via callout, mirroring the shim's ownkfree_rcu()delay-line convention (sys/dev/drm/linux_rcu.c).build.sh,run.shβ exact commands.build.log,run.log,run.stress.log,run.fixed.logβ untrimmed logs.VERDICT.md,manifest.json,verdict.jsonβ outcome.
Reproduce
On the guest (amplifier kernel installed):
sh build.sh # builds idr_race.ko sh run.sh # amplified: expect CAPTURE; native: expect none
Fix validation: restore pristine linux_idr.c, apply amplifier.diff +
fix.diff, rebuild kernel, reinstall KLD, rerun run.sh β captures must
stay 0 while conditions are otherwise identical.
DF-4506 VERDICT
Finding
sys/dev/drm/i915/i915_gem_context.c resolves user-supplied context handles
with no lock at all β destroy :831 (struct_mutex acquired only at
:835, after the lookup), getparam :855, setparam :901 (neither ever
takes struct_mutex), and reset_stats :981 (bare rcu_read_lock()).
On Linux this is safe because IDR reads are RCU-protected. On DragonFly the
entire safety fiction collapses:
rcu_read_lock()==preempt_disable()==cpu_ccfence()β a no-op with no grace period (sys/dev/drm/include/linux/rcupdate.h:46-56,sys/dev/drm/include/linux/preempt.h:33);idr_find()takes no token (sys/libkern/linux_idr.c:515-528);idr_grow()β reached from this very file's create path (i915_gem_context.c:355->idr_alloc()when the 255-entry array is full β swaps in a new node array and kfrees the old one synchronously (sys/libkern/linux_idr.c:449-459).
A reader that loads idp->idr_nodes before the swap dereferences freed
memory after the kfree. The freed 4080-byte array is prime grooming
material: if re-allocated with controlled contents, idr_find() hands the
ioctl an attacker-crafted struct i915_gem_context *, which flows into
kref_get_unless_zero(&ctx->ref) (refcount write at controlled offset) and,
on the destroy path, into context_close() -> set_bit on ctx->flags,
list_del_init(&ctx->hw_id_link) (unlink write primitive), lut_close()
list/radix walks on controlled pointers β i.e. pointer-driven kernel write
primitives from an unprivileged render-node user.
This is beyond the known DF-1662 (destroy-vs-destroy double-close): here no double anything is needed β one ordinary create racing one ordinary lookup ioctl suffices, because the locking assumption the code inherits from Linux (RCU-safe IDR) does not exist on DragonFly.
Verification (guest: DragonFly 6.5-DEVELOPMENT, 6 vCPU, no i915 GPU)
The guest has virtio-vga only, so the i915 ioctl surface itself cannot run.
The PoC therefore drives the same primitives the ioctls use β
idr_init/idr_alloc/idr_find/idr_destroy β from a KLD
(idr_race.c), with a producer thread that fills the idr to the grow
threshold, triggers the grow (kfree of the old array) and immediately
re-allocates the freed size-class filled with a 0xAA canary, racing a
consumer thread doing lockless idr_find().
- Amplified (guest-only sysctl-gated
tsleepbetweenidr_find's node-pointer load and its dereference β an interleaving the uninstrumented code already permits; the amplifier only widens a ~2-5ns window): 20/20 and 9/10 rounds returned the canary pointer 0xAAAAAAAAAAAAAAAA βidr_find()returned a pointer crafted by the re-allocation of the freed array. Controlled UAF read proven. - Native, no amplifier: 500-round stress with a concurrent reader ->
kernel panic:
Fatal trap 12 ... Stopped at idr_find+0x48: movl 0x8(%rbx),%edx, fault VA 0xc88 ==nodes[200].allocatedoff a stale pointer (panic.txt). The race window is practically winnable without instrumentation. - Fix validation (
fix.diff: defer the kfree of replaced/destroyed node arrays by 1s via callout, mirroring the shim's ownkfree_rcudelay-line convention insys/dev/drm/linux_rcu.c): rebuilt kernel with identical amplifier conditions -> 0 captures, 0 anomalies; 3x native stress with 1,598,684 finds -> no panic, guest alive (run.fixed.log).
Bottom line
- Root cause certain, race demonstrably reachable at native stress rates (panic), controlled-pointer UAF read demonstrated under amplification.
- Demonstrated impact on this guest: kernel panic. In the real i915 path
(Intel GPU present, unprivileged render node) the same primitive yields
an attacker-influenced
i915_gem_context *consumed by write-heavy close/destroy logic β a memory-corruption escalation path; the full chain to uid=0 was not assembled because the audit guest has no Intel GPU. fix.diffis validated: baseline reproduces (captures + panic), patched kernel is clean under the same conditions.
Cross-references
- DF-1662 (pass 1): destroy-vs-destroy double-close refcount underflow β distinct bug, same ioctl; both are fixed/hardened by serializing lookups.
- DF-4444 (i915_vma drm_vma_node_unmap no-op): orthogonal; contexts'
lut_closeonly drops object refs β no additional interaction found (i915_gem_context.c:97-116vsi915_vma.c).
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
fixedfix.diff defers the kfree of replaced/destroyed IDR node arrays by 1s via callout, mirroring the drm shim's own kfree_rcu delay-line convention. Baseline kernel: 20/20+9/10 amplified canary captures and a native-window kernel panic in idr_find+0x48. Patched kernel under identical amplified conditions: 0 captures/0 anomalies; 3x native stress (1,598,684 finds): no panic, guest alive. fix_status=fixed.
run.fixed.log (0 captures after fix under identical conditions), build.log (both kernel builds), panic.txt (baseline panic)
Confirmed kernel references
- sys/dev/drm/i915/i915_gem_context.c:831
- sys/dev/drm/i915/i915_gem_context.c:855
- sys/dev/drm/i915/i915_gem_context.c:901
- sys/dev/drm/i915/i915_gem_context.c:981
- sys/dev/drm/i915/i915_gem_context.c:355
- sys/dev/drm/i915/i915_drv.h:3288-3306
- sys/libkern/linux_idr.c:515-528
- sys/libkern/linux_idr.c:420-461
- sys/dev/drm/include/linux/rcupdate.h:46-56
- sys/dev/drm/include/linux/preempt.h:33
- sys/dev/drm/linux_rcu.c:161-184
Detail
Exploit chain
unpriv user opens render node; T1 spams DRM_IOCTL_I915_GEM_CONTEXT_CREATE until >255 live contexts force idr_grow (kfree of old 4080B node array) while grooming that size class with controlled contents; T2 simultaneously spams CONTEXT_DESTROY/GETPARAM/SETPARAM/RESET_STATS whose lockless i915_gem_context_lookup walks the freed array; won race returns crafted ctx pointer -> kref_get_unless_zero write at controlled offset; on destroy path __destroy_hw_context -> context_close -> set_bit(ctx->flags), list_del_init(ctx->hw_id_link) unlink writes, lut_close list/radix walks on controlled pointers -> arbitrary write -> cred overwrite -> uid=0. Demonstrated on guest: controlled UAF read (canary) amplified 20/20+9/10, native panic in idr_find; pointer-to-write chain analyzed at i915_gem_context.c:278-299/395-400 but not executed end-to-end (no i915 hw on guest).
Evidence (decisive lines)
panic.txt (Fatal trap 12, Stopped at idr_find+0x48: movl 0x8(%rbx),%edx, VA 0xc88=nodes[200].allocated); run.log (amplified canary captures 20/20 then 9/10); run.fixed.log (0 captures, 1,598,684 native finds, guest alive after fix); idr_race.c + amplifier.diff + fix.diff; VERDICT.md for the full narrative
PoC changes
no orchestrator seed existed; pack built from scratch: KLD reproducer (producer=fill+grow+canary-realloc, consumer=lockless idr_find, canary detector), guest-only sysctl-gated race amplifier in idr_find, and the real fix.diff (deferred kfree via callout). Guest quirks handled: no buildworld (used nativekernel), kmod.mk path, -Wcast-qual, DF kthread_create signature, and reader lwkt_yield() to avoid starving the non-preemptive scheduler.
Verified recommended fix
Defer kfree() of IDR node arrays in idr_grow()/idr_destroy() by ~1s (callout), matching the drm shim's kfree_rcu delay-line convention; optionally also serialize i915 context lookups under dev->struct_mutex.
Verdict
i915_gem_context.c performs its handle->context lookups (destroy :831, getparam :855, setparam :901, reset_stats :981) with no lock, inheriting Linux's assumption that idr_find() is RCU-safe. On DragonFly rcu_read_lock() is a no-op and idr_find() takes no token, while idr_grow() (reachable from the same file's create path :355) kfrees the old node array synchronously β an unprivileged render-node user racing create against any lookup ioctl gets a UAF read that returns attacker-groomed contents as a struct i915_gem_context * (feeding kref_get_unless_zero, set_bit, list_del_init write primitives). Verified on the guest: amplified runs returned the canary pointer in 20/20 and 9/10 rounds; native unamplified 500-round stress PANICKED the kernel in idr_find+0x48 (fault 0xc88 = nodes[200].allocated); fix.diff (defer the kfree 1s, mirroring the shim's kfree_rcu delay line) rebuilt under identical conditions gave 0 captures and no panic across 1,598,684 native finds. Full uid=0 chain not assembled: the guest has no Intel GPU, so the i915 ioctl surface itself is not exercisable here.
No comments yet.