# 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:

- `rcu_read_lock()` is `cpu_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: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 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.
