# DF-0615 — VERDICT

**Verdict: REPRODUCED (use-after-free / kernel-heap info-leak via unsynchronized address-selection policy table). Fix VALIDATED on a built single-fix kernel.**

## What the bug is

The global `TAILQ addrsel_policytab` (`sys/netinet6/in6_src.c:728`) — the IPv6
RFC-3484 address-selection policy table — has **no lock, token, or
serialization of any kind**. It is concurrently:

- **read** by `walk_addrsel_policy` (`in6_src.c:786`), a `TAILQ_FOREACH` that
  dereferences each entry and, via the callback `dump_addrsel_policyent`
  (`in6_src.c:801`), calls `SYSCTL_OUT` → `sysctl_old_user` → `copyout`
  (`kern_sysctl.c:1337`) for each entry. The sysctl
  `net.inet6.ip6.addrctlpolicy` (`in6_src.c:665`, `CTLFLAG_RD`,
  world-readable) routes here through `in6_src_sysctl` (`in6_src.c:669`), which
  runs **on the calling user's thread** (`userland_sysctl`, `kern_sysctl.c:1519`,
  does not dispatch to netisr). So **the read side is unprivileged**.
- **mutated** by `add_addrsel_policyent` (`in6_src.c:737`,
  `TAILQ_INSERT_TAIL`) and `delete_addrsel_policyent` (`in6_src.c:763`,
  `TAILQ_REMOVE` + `kfree(pol)` at lines 779-780), reached from
  `in6_src_ioctl` (`in6_src.c:682`). `SIOCAADDRCTL_POLICY` /
  `SIOCDADDRCTL_POLICY` are dispatched to **netisr0** (`in6.c:456`,
  `lwkt_domsg(netisr_cpuport(0), ...)`) after a privilege check
  (`in6.c:510`, `caps_priv_check_td(SYSCAP_RESTRICTEDROOT)`). So **the mutate
  side needs root** (a concurrent root reconfiguration — admin, `ip6addrctl`,
  `rtsold`/`racoon`, etc.).

There is no synchronization between the two sides. The reader's `TAILQ_FOREACH`
holds a `pol` pointer across the blocking `copyout`; a concurrent deleter on
netisr0 frees that very entry. When the reader resumes,
`pol = TAILQ_NEXT(pol, ape_entry)` reads `pol->ape_entry.tqe_next` — the word at
**offset 0** of the freed chunk.

## The use-after-free read (confirmed mechanism)

`kfree` in DragonFly's slab allocator (`kern_slaballoc.c:1584-1585`) links the
freed chunk onto the per-zone free list by writing the previous free-list head
into **offset 0** of the chunk:

```c
1584:    chunk->c_Next = z->z_LChunks;
1585:    z->z_LChunks = chunk;
```

`struct addrsel_policyent`'s first field is `TAILQ_ENTRY(addrsel_policyent)
ape_entry`, whose `tqe_next` is also at **offset 0**. So after the concurrent
`kfree(pol)`, the reader's `TAILQ_NEXT(pol)` returns the **slab free-list
pointer**, not the real list successor. The walk then **follows the free-list
chain through freed chunks**, calling `SYSCTL_OUT` on each one's body (offset
16+, `ape_policy`) and copying that freed-heap data to userspace.

This is a textbook **use-after-free read (CWE-416)** of kernel heap memory
disclosed to an unprivileged user, caused by a **race condition (CWE-362)** —
exactly as the finding claims.

## Reproduction on the unpatched master DEV kernel

Guest: DragonFly 6.5-DEVELOPMENT `#0` (X86_64_GENERIC, INVARIANTS, 6 vCPU).

The table is **always populated**: the boot rc.d script `ip6addrctl`
(`etc/rc.d/ip6addrctl`) installs **9 RFC-3484 default policy entries** at boot,
so an unprivileged reader always has entries to walk.

PoC = `reader.c` (unprivileged sysctl reader, tight loop) + `mutator.c` (root,
adds 64 distinct `2001:XX00::/24` entries then churns delete+re-add in a tight
loop) + `race.sh` (launches 6 readers as `maxx` + 1 mutator as root).

**Decisive evidence (`run.log`, 16 s race, unpatched `#0`):** all 6 readers
observe the sysctl return **more entries than can possibly exist** — the real
table tops out at 75 entries (9 defaults + 64 churned + 2 slack), yet readers
saw up to **154 entries**, with **48 total anomaly lines** across the readers.
The excess entries are bytes copied from **freed slab chunks** the reader
walked via the corrupted free-list pointer.

**One-shot leak capture (`leak_capture.log`, `capture.c`):** caught a UAF at
iter 265 — the sysctl returned **122 entries** (47 beyond the real table),
leaking **3384 bytes** of freed `addrsel_policyent` bodies (stale
labels/precedence/addresses from previously-deleted mutator entries, saved to
`leak.bin`, hex in `leak_hex.txt`). This is freed kernel heap disclosed to an
unprivileged user.

Sample leaked bytes (entries [75..121], from freed chunks):
```
leaked[0] fam=28 label=21 preced=61 | hex: 1c 1c 00 00 00 00 00 00 20 01 15 00 ...
leaked[1] fam=28 label=22 preced=62 | hex: 1c 1c 00 00 00 00 00 00 20 01 16 00 ...
```
(`1c 1c` = `sin6_len=28, sin6_family=AF_INET6`; these are stale freed-mutator
entries with label=i, preced=40+i.)

## Why it does not panic on this kernel (honest characterization)

The finding's headline mentions "panic or info-leak". On this INVARIANTS DEV
kernel with the default `debug.use_weird_array=0`, the UAF manifests as the
**info-leak** above, **not** a panic: the freed chunk's offset-0 word is the
slab free-list pointer (a valid in-zone address), so the reader follows a chain
of valid mapped freed chunks until it hits `NULL` and the walk ends. A
**panic** would require the freed chunk's page to be reclaimed by the VM
(released from the slab depot back to the page allocator) before the reader
reads it — the slab depot's caching prevents this under steady churn, so a
crash is not reliably reachable on this configuration. We confirmed
`debug.use_weird_array=1` does **not** force a crash either: `kfree` writes the
free-list linkage at offset 0 *after* the weird poison (`kern_slaballoc.c:1567`
then `:1584`), so `tqe_next` remains a valid free-list pointer regardless.

**Impact is therefore a reliable, repeatable kernel-heap info-leak to an
unprivileged local user** (up to ~3.4 KB per race hit, repeatedly; contents are
freed slab data which, under a different heap state, could include other
128-byte-bucket kernel objects' contents). This is a real security defect; the
"speculative code execution" angle in the finding is not demonstrated here and
would require slab cross-zone reclamation + controlled reclaim content.

## Exploit chain

Not a memory-corruption *write* primitive (the UAF is a read). No escalation
chain developed — the primitive is a freed-heap **read**. Documented impact
ceiling: repeatable disclosure of freed kmalloc-128-bucket heap data to an
unprivileged user. (`exploit_chain = none` for the JSON, as this is not a
corruption/write class.)

## PoC changes (what I changed and why)

The reviewer's scaffold was a correct *sketch* but did not reproduce on its
own:

- **`reader.c`** — rewrote to (a) drop `madvise(MADV_DONTNEED)` (it broke the
  sysctl two-pass size negotiation on this guest, making reads return 0), (b)
  drop `usched_set` CPU-pinning (`USCHED_SET_CPU` is `EPERM` for unprivileged
  users on DragonFly — it silently failed and, combined with the static buffer,
  left the reader seeing only the 9 boot-default entries), (c) add unambiguous
  anomaly detection: flag any sysctl return with **more entries than the real
  table can hold** (`> NENT+DEFAULTS+2 = 75`) or with impossible fields, and
  hex-dump the leaked bytes.
- **`mutator.c`** — rewrote to populate **64 distinct** prefixes
  (`2001:XX00::/24`, label=i) then churn delete+re-add in rotation, keeping the
  table densely populated and constantly freeing entries (the original added
  only one entry, which the duplication check made a no-op after the first add
  — far too little churn to hit the window).
- **`capture.c`** — new one-shot helper that waits for a UAF read and saves the
  raw leaked bytes (`leak.bin`) for evidence.
- **`race.sh`** — new root-run coordinator: launches 6 readers via `su maxx`
  + 1 root mutator, races them, collects anomaly counts. (Original scaffold had
  no coordinator; the unprivileged-reader / privileged-mutator split needs a
  root orchestrator.)
- **`build.sh` / `run.sh`** — added exact reproduce scripts.

## Fix (`fix.diff`)

Author an `lwkt_token` (`addrsel_policy_token`, `LWKT_TOKEN_INITIALIZER`,
matching the pattern at `mld6.c:110`) acquired around all three accessors:

- `walk_addrsel_policy` — `lwkt_gettoken` before the `TAILQ_FOREACH`,
  `lwkt_reltoken` on return (held across the blocking `SYSCTL_OUT`; lwkt tokens
  are held across blocking copyout in DragonFly's model).
- `add_addrsel_policyent` — move the `M_WAITOK` `kmalloc` **before** taking the
  token (must not sleep holding a token-candidate under contention), then take
  the token for the dup-check + `TAILQ_INSERT_TAIL`.
- `delete_addrsel_policyent` — take the token around search +
  `TAILQ_REMOVE` + `kfree`.

This **matches the finding's recommended fix** (same `lwkt_token` approach,
same three sites, same M_WAITOK-before-token ordering refinement). Minimal,
targeted at the root cause.

## Fix validation (Phase 8) — VALIDATED

- **Baseline (`#0`, unpatched):** re-confirmed — 48 anomaly lines, readers saw
  up to 154 entries (>75 real), `capture.c` leaked 3384 bytes from freed
  chunks.
- **Applied `fix.diff`** to in-guest `/usr/src` (all 5 hunks applied), built
  `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (`NK_DONE rc=0`,
  `fix_build.log`), `make installkernel`, rebooted →
  `DragonFly 6.5-DEVELOPMENT #1` (sha256 `ae346c44…`).
- **Patched (`#1`, with fix):** ran the **same** race for 25 s → **0 anomaly
  lines**, every reader `maxent=73` (exactly the real table size: 9 defaults +
  64 churned). No entry count ever exceeded the real table. **The UAF is gone.**

The token serializes the reader walk against the deleter's free, closing the
race. Clean before/after.

## Verified kernel references

- `sys/netinet6/in6_src.c:728` — unlocked global `addrsel_policytab`.
- `sys/netinet6/in6_src.c:737` / `:757` — `add_addrsel_policyent` (insert, no lock).
- `sys/netinet6/in6_src.c:763` / `:779-780` — `delete_addrsel_policyent` (`TAILQ_REMOVE` + `kfree`).
- `sys/netinet6/in6_src.c:786` / `:791-795` — `walk_addrsel_policy` (`TAILQ_FOREACH`, no lock).
- `sys/netinet6/in6_src.c:806` — `SYSCTL_OUT` blocking copyout in the callback.
- `sys/netinet6/in6_src.c:665-669` — `CTLFLAG_RD` world-readable sysctl node.
- `sys/netinet6/in6.c:422-456` — ioctl dispatch to netisr0.
- `sys/netinet6/in6.c:510-512` — privilege check + `in6_src_ioctl` call.
- `sys/kern/kern_sysctl.c:1337` — `sysctl_old_user` → `copyout` (blocking read path).
- `sys/kern/kern_slaballoc.c:1584-1585` — `kfree` free-list linkage at offset 0 (the UAF sink).
