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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4506 Β· 15 files
FileTypeDescriptionSize
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() at i915_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_rcu under bare rcu_read_lock())

On Linux this is safe because IDR reads are RCU-protected. On DragonFly:

=> 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 = lockless idr_find, detector for the 0xAA canary pointer)
  • amplifier.diff β€” guest-only instrumentation: sysctl-gated tsleep between idr_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 own kfree_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.

VERDICT.md
↓ download raw

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:

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().

  1. Amplified (guest-only sysctl-gated tsleep between idr_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.
  2. 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].allocated off a stale pointer (panic.txt). The race window is practically winnable without instrumentation.
  3. Fix validation (fix.diff: defer the kfree of replaced/destroyed node arrays by 1s via callout, mirroring the shim's own kfree_rcu delay-line convention in sys/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.diff is 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_close only drops object refs β€” no additional interaction found (i915_gem_context.c:97-116 vs i915_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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-4506

Confirmed kernel references

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.