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

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.
