# DF-5033 — UAF/OOB-read race: `amdgpu_bo_list_get()` vs `idr_grow()` (DF idr is not RCU-safe for lookups)

## What

`amdgpu_bo_list_get()` (sys/dev/drm/amd/amdgpu/amdgpu_bo_list.c:164-177) uses
the Linux idiom for RCU-safe IDR lookups:

	rcu_read_lock();
	*result = idr_find(&fpriv->bo_list_handles, id);
	if (*result && kref_get_unless_zero(&(*result)->refcount)) ...

On Linux this is correct: idr is built on the RCU-safe xarray.  On
DragonFly the DRM-compat idr (sys/libkern/linux_idr.c) is a **flat array**
guarded by an `lwkt_token`, and:

  * `idr_find()` (linux_idr.c:516-529) takes **no lock at all** — it reads
    `idp->idr_count` and `idp->idr_nodes[id]` bare;
  * `idr_grow()` (linux_idr.c:421-459, called under the token from
    `idr_alloc()`, linux_idr.c:358) swaps in a new node array
    (`idp->idr_nodes = newnodes`, :454) and **immediately `kfree()`s the old
    array** (:458) — no RCU grace period, no deferral.

So a CS ioctl thread (amdgpu_cs.c:591 calls `amdgpu_bo_list_get()` with no
`bo_list_lock`) races a concurrent `AMDGPU_BO_LIST_OP_CREATE` ioctl
(amdgpu_bo_list.c:283 `idr_alloc()` → `idr_grow()` → `kfree(oldnodes)`).
The per-fd idr grows when the 255-node initial array
(`IDR_DEFAULT_SIZE`, linux_idr.c:132) overflows — i.e. after an
unprivileged render-node user creates >254 BO lists in one fd — and again at
511, 1023, … entries.

Two unsynchronized interleavings of the unlocked two-step read in
`idr_find()` (bound-check `idr_count`; load `idr_nodes`; dereference):

  * **V1 (preemption in window)** — reader loads `idp->idr_nodes` (old
    array), is preempted; grower swaps + frees; reader resumes and
    dereferences the freed 4080-byte chunk → **use-after-free read**.  The
    garbage/foreign `data` pointer it returns is then handed to
    `kref_get_unless_zero()` (amdgpu_bo_list.c:170) = an atomic RMW on
    freed/garbage memory (refcount corruption; M_IDR-zone reuse is limited
    to other idr arrays, so the read value is a foreign-but-real
    `amdgpu_bo_list*` or a stale pointer to an already-freed list — the
    latter gives the atomic increment on freed memory, and a later
    balanced `amdgpu_bo_list_put()` double-drop/UAF chain).
  * **V2 (load reorder)** — nothing in `idr_find()` forbids the compiler
    from loading `idp->idr_nodes` before `idp->idr_count` (independent
    loads, no barriers, no READ_ONCE).  Reader caches the old 255-entry
    pointer, observes the new `idr_count` (e.g. 1023), passes the bound
    check with id=400 and reads `nodes[400]` → **OOB read 2320 bytes past
    the freed 4080-byte chunk** plus UAF.

Note: the *object* half of the pattern is safe on DF —
`amdgpu_bo_list_free()` releases via `call_rcu()`, and DF's `call_rcu`
(sys/dev/drm/linux_rcu.c) defers callbacks ≥1 s — but the *idr array* half
is not.

## Files in this pack

  * `idr_test_core.c`   — GENERATED from sys/libkern/linux_idr.c by
     `gen_core.py`: only the leading `#ifdef USERLAND_TEST/#else/#endif`
     header block is replaced with `#include "harness_prelude.h"`;
     every algorithm line is **byte-identical** to the kernel source
     (verified by diff at packaging time; detected line numbers map 1:1).
  * `harness_prelude.h`, `inc/sys/idr.h` — build-time stubs (same role the
     file's own USERLAND_TEST block plays) + instrumentation hooks.
     `IDR_FIXED_BUILD` maps `lwkt_gettoken/reltoken` onto a real mutex.
  * `idr_race.c` — concurrent harness: N reader threads call `idr_find()`
     with no lock (exactly `amdgpu_bo_list_get()`'s semantics) while a
     grower drives `idr_alloc()` growth phases (the CREATE ioctl's
     semantics).  `kfree` is instrumented to poison freed chunks (0xDD) and
     quarantine them, so any read of a freed `idr_nodes[]` is detectable.
     Readers park on a gate while the idr is destroyed/re-initialized, so
     every observed free during a grow phase comes from `idr_grow()` — the
     kernel-reachable event (idr_destroy runs at fd close where drm core
     excludes in-flight ioctls).
  * `oracle.c` — deterministic interleaving proof of V1 and V2 against the
     same byte-identical core.
  * `build.sh`, `run.sh` — exact commands.
  * `oracle_baseline.log`, `oracle_fixed.log`, `race_baseline.log`,
     `race_fixed.log`, `env.txt` — full untrimmed guest output.
  * `fix.diff` — call-site fix (serialize with `bo_list_lock`).

## Build / run (on the DragonFly guest)

    sh build.sh          # 4 binaries: oracle, oracle_fixed, idr_race, idr_race_fixed
    sh run.sh            # runs all four, tee'ing logs

## Expected output

`oracle` (baseline, idr_find unlocked exactly as in the kernel):

    V1 preemption-in-window: allocated=dddddddd data=dddddddddddddddd -> *** USE-AFTER-FREE READ of freed idr_nodes[] ... ***
    V2 load-reorder:          allocated=dddddddd data=dddddddddddddddd -> *** OOB READ past freed 255-entry chunk ... ***
    BASELINE RESULT: both interleavings dereference freed idr_nodes[] memory

`oracle_fixed` (idr_find takes the token — the fix, algorithm level):

    V1 call-after-grow idr_find(4) = 5100000000000003 -> clean (valid tag / NULL)
    V2 call idr_find(400)            = 510000000000012b -> clean (valid tag / NULL)
    FIXED RESULT: no unsynchronized read possible; both variants clean

`idr_race` (concurrent, 400 grow phases = 1600 grow-kfree events):
no spontaneous hit was observed — consistent with the ~2-cycle window vs
microsecond-scale free; the in-kernel harm window is preemption/SMT-stall
inside those 2 cycles or the compiler-reorder variant, which the oracle
covers deterministically.  `idr_race_fixed` completes the same workload with
zero anomalies (regression check for the fix).
