# DF-5689 — idr_find()/idr_for_each() are lockless against idr_grow()'s kfree of the old node array (UAF read)

**File:** `sys/libkern/linux_idr.c` (pass 2)
**Class:** kernel use-after-free read (memcorrupt / kernleak)
**Evidence class (per ID reservation):** same-source userland race harness with a poisoned `kfree` canary (the guest has no ASAN), stock vs fixed source from `/usr/src`.

## Build (on the DragonFly guest, as root)

```
sh build.sh     # builds df5689_race_stock (pristine linux_idr.stock.c,
                # md5 2d66ecf755e254d38a5c9e9e63992eaa)
                # and df5689_race (/usr/src/sys/libkern/linux_idr.c —
                # stock for the baseline phase, fixed after fix.diff is
                # applied for the validation phase)
```

## Run

```
./df5689_race_stock 15            # baseline: 2 find-threads + writer cycling
                                  # grow 255->511->1023->2047 (3 kfrees/cycle)
./df5689_race_stock 10 foreach    # adds the idr_for_each reader (usually
                                  # SIGSEGVs: OOB read past the freed old
                                  # array — for_each caches nodes but re-reads
                                  # idr_count each iteration)
./df5689_race 15                  # fixed source: same load, expect zero hits
```

## Expected

* stock: within seconds —
  `UAF-READ: idr_find(<id>) returned data from a FREED idr_nodes array
  (0xdddddddddddddddd)` (the shim allocator poisons every kfree'd chunk;
  that pointer value can only be observed by reading freed memory).
  Observed on every baseline round; the for_each variant additionally
  crashes with SIGSEGV (out-of-bounds read past the freed chunk).
* fixed (fix.diff: idr_find takes idr_token; idr_for_each takes idr_token
  and tests leaf state): **zero hits across 37.4M racing idr_find calls**
  in 3 rounds (14.6M/13.3M/9.5M) against 102k poisoned grow-kfrees.

## In-kernel meaning

`idr_find()` returns `nodes[id].data` from the freed array — i.e. a
**stale kernel object pointer read from freed slab memory is handed back
to the caller**. In-tree unguarded consumer:
`i915_gem_object_lookup_rcu()` (sys/dev/drm/i915/i915_gem_object.h:307-314)
calls `idr_find()` under `rcu_read_lock()` only
(sys/dev/drm/i915/i915_gem.c:4532-4533, 4971-4972); DragonFly `kfree()`
has no RCU grace period, so the Linux RCU argument does not transfer.
The stale `struct drm_i915_gem_object *` then flows into
`i915_gem_object_get_rcu()`/use — a UAF-deref of attacker-influenceable
slab content (object confusion), plus a kernel-pointer disclosure
primitive (freed-chunk contents returned through the ioctl).

The guest has no GPU (`/dev/dri` absent), so the i915/ioctl-level chain
cannot be driven there; the same-source race harness is the reserved
evidence class for this file and establishes the UAF read conclusively
(poison canary + OOB-SEGV manifestation).

## Files

* `df5689_race.c`     the race harness (includes the real linux_idr.c)
* `linux_idr.stock.c` pristine copy of the audited source
* `shim/`             userland stub headers (tokens = recursive pthread
                      mutexes; kfree poisons)
* `run.*.log`         baseline and fix-validation outputs
* `fix.diff`          validated fix (same diff as DF-5688's — shared file)
