# DF-0615 — PoC: addrsel_policytab UAF race (reproduced)

Unsynchronized IPv6 address-selection policy table → **use-after-free read /
kernel-heap info-leak** to an unprivileged user, racing a world-readable
sysctl reader against a privileged (root) policy-table mutator.

## Status

**REPRODUCED** on DragonFly 6.5-DEVELOPMENT `#0` (X86_64_GENERIC). Fix
**VALIDATED** on a single-fix kernel (`#1`, lwkt_token serialization): the
UAF/leak is gone. See `VERDICT.md` for the full analysis.

## Files

- `reader.c` — unprivileged user; tight `sysctlbyname("net.inet6.ip6.addrctlpolicy")`
  loop with UAF anomaly detection (flags reads returning more entries than the
  real table can hold + hexdumps leaked bytes).
- `mutator.c` — root; adds 64 distinct `2001:XX00::/24` entries then churns
  delete+re-add. `delete_addrsel_policyent` (`in6_src.c:779-780`) does
  `TAILQ_REMOVE` + `kfree`; racing the reader's walk yields the UAF.
- `capture.c` — one-shot: waits for a UAF read and saves the leaked freed-chunk
  bytes to `/tmp/df0615_leak.bin`.
- `race.sh` — root coordinator: 6 readers (as `maxx`) + 1 mutator (root).
- `fix.diff` — `git apply`-able fix: `lwkt_token` around all three accessors.

## Build & run

```
# build (as maxx, no privilege needed)
./build.sh

# run (as root — the mutator needs privilege; race.sh su's to maxx for readers)
sudo ./run.sh 16
```

## Expected outcome

**Unpatched (`#0`):** within ~1-3 s, readers print
`UAF#1: sysctl returned N entries (> 75 real) -- walked freed slab chunks`
with `maxent` climbing to 120-154 (real table max is 75: 9 RFC-3484 boot
defaults + 64 churned). `capture.c` writes `leak.bin` with up to ~3.4 KB of
freed `addrsel_policyent` data leaked from the slab free list.

**Patched (`#1`, `fix.diff`):** `0 anomalies`, `maxent=73` (exactly the real
table size). No freed-chunk traversal.

## How the UAF works

`kfree` links the freed chunk onto the slab free list by writing the previous
free-list head into **offset 0** of the chunk (`kern_slaballoc.c:1584`,
`chunk->c_Next = z->z_LChunks`). `struct addrsel_policyent`'s
`TAILQ_ENTRY ape_entry` is also at offset 0. So after a concurrent `kfree(pol)`,
the reader's `TAILQ_NEXT(pol)` returns the **slab free-list pointer**, and the
walk follows the free-list chain through freed chunks, copying their bodies
(offset 16+, `ape_policy`) to userspace via the blocking `SYSCTL_OUT` copyout.

## Notes

- The reader is unprivileged; the mutator requires root
  (`SIOCAADDRCTL_POLICY`/`SIOCDADDRCTL_POLICY` are gated by
  `caps_priv_check_td(SYSCAP_RESTRICTEDROOT)` at `in6.c:510`).
- The sysctl handler runs on the caller's user thread; the ioctl mutator runs
  on netisr0 (`in6.c:456`) — different CPUs, so the race is cross-CPU.
- The table is always populated: `ip6addrctl` installs 9 RFC-3484 defaults at
  boot, so a reader always has entries to walk.
- Impact on this kernel is the info-leak (freed-heap read); a panic would need
  slab-page reclamation, which the depot cache prevents under steady churn.
