# DF-0417 — Use-after-free race on `nd_defrouter` via `defrtrlist_update()`

**File:** `sys/netinet6/nd6_rtr.c`  ·  **Severity:** High  ·  **Class:** UAF / refcount-lifetime race

## Verdict

**REPRODUCED** as a kernel panic (network-reachable UAF → DoS). The race the
finding describes is real and confirmed both by source-level tracing and by a
live crash. A targeted refcount fix eliminates the UAF; the fix is validated
by building a single-fix kernel and confirming the UAF panic no longer
reproduces under the identical workload.

## The bug (mechanism, path:line)

`defrtrlist_update()` (`sys/netinet6/nd6_rtr.c:661-707`) looks up or allocates
a `struct nd_defrouter`, takes **no reference** on it, and returns it to
`nd6_ra_input()` with `nd6_mtx` **released** (`nd6_rtr.c:679`, `685`, `692`,
`705`):

```c
dr = defrtrlist_update(&dr0);          /* nd6_ra_input, line 285 */
...
for (...prefix options...)              /* lines 291-345, NO lock held */
    prelist_update(&pr, dr, m);         /* line 344 */
```

`prelist_update()` (`nd6_rtr.c:855`) re-acquires `nd6_mtx` (line 866) and
stores `dr` into a prefix's advertising-router list via `pfxrtr_add()` →
`new->router = dr` (`nd6_rtr.c:728`).  Between line 285 (lock released) and
line 866 (lock re-acquired) a concurrent deleter can free `dr`:

- another RA with `rtlifetime == 0` for the same router → `defrtrlist_del(dr)`
  (`nd6_rtr.c:671`), or
- `nd6_timer` expiry (`nd6.c:582`), or
- `nd6_purge` on interface detach (`nd6.c:801/805`), or
- a peer dropping its router flag (`nd6_nbr.c:870`).

`defrtrlist_del()` (`nd6_rtr.c:541`) does `TAILQ_REMOVE` + prefix cleanup +
`kfree(dr, M_IP6NDP)` (`:576`).  If it runs **before** `pfxrtr_add()` stores
the pointer, the stored `pfr->router` is a **dangling pointer** to freed
memory.  It is later field-dereferenced in three deferred sinks:

- `find_pfxlist_reachable_router()` — `nd6_rtr.c:1148-1149` (`pfxrtr->router->rtaddr`, `->ifp`)
- `nd6_sysctl_prlist()` — `nd6.c:2248-2253` (`pfr->router->rtaddr`, `->ifp`)
- `nd6_sysctl_drlist()`/prefix export — `nd6.c:1493`

`struct nd_defrouter` (`nd6.h:233`) has **no refcount** — the root cause.

## Live reproduction

The victim is any IPv6 host with RA acceptance enabled
(`net.inet6.ip6.accept_rtadv=1`, the normal IPv6-autoconfig host mode).  An
on-link attacker floods Router Advertisements — alternating CREATE (lifetime
1800 + many prefix-info options to widen the unlocked window) and DELETE
(lifetime 0) for the same router address.  The prefix-info options stretch the
window between `defrtrlist_update()` returning (lock released) and
`prelist_update()` re-locking, maximising the chance a concurrent DELETE frees
the router mid-loop.

The PoC (`ra_race_mt.c`) simulates the on-link attacker: it injects crafted
unicast RAs into a `tap` interface (writing `/dev/tapN` calls `if_input`
synchronously on the writer's CPU — `if_tap.c:981` — so multiple worker threads
on different CPUs process create/delete RAs concurrently).  A concurrent
`ndp -p` reader triggers the deferred `pfxrtr->router` dereference.

**Decisive run (unpatched GENERIC `#0`, with-src):** guest down within ~4 s;
serial console (`boot.log`):

```
Fatal user address access from kernel mode from ndp at ffffffff807e7295
Fatal trap 12: page fault while in kernel mode
cpuid = 4; lapic id = 4
fault virtual address  = 0x48
fault code             = supervisor read data, page not present
instruction pointer    = 0x8:0xffffffff807e7295
current process        = 1118   (ndp -p)
kernel: type 12 trap, code=0
Stopped at nd6_sysctl_prlist.part.10+0x165: movq 0x48(%rsi),%rdx
```

`nd6_sysctl_prlist` (`nd6.c:2248`) dereferenced the dangling `pfr->router` →
page fault → panic.  This is exactly the deferred UAF sink the finding cites.

## Impact

Network-reachable **kernel panic / DoS** on default GENERIC (INVARIANTS ON):
the freed slab is poisoned, so the deferred read faults.  On a non-INVARIANTS
kernel the read silently returns whatever reoccupied the slab (potential
limited info-leak of recycled `M_IP6NDP` memory).  The primitive is a
dangling-pointer **read** in a non-control-data context (the `rtaddr`/`ifp`
fields are used for display/routing, not as a write or control-flow target),
so the realistic ceiling is **DoS + possible info-leak**, not code execution /
uid-0.  This is a network-reachable bug (not local-unpriv→root), so the
uid-0 escalation bar does not apply.

## The fix (`fix.diff`)

Add reference counting to `struct nd_defrouter`:

1. `nd6.h` — add `int refcnt;` (the sysctl exports copy individual fields into
   a separate `struct in6_defrouter`, so this changes no userspace ABI).
2. `nd6_rtr.c` — `nd_defrouter_hold()`/`nd_defrouter_rele()` helpers using
   `atomic_add_int` / `atomic_fetchadd_int` (free when the last ref drops).
3. A router entry gets **one ref for list membership** (init to 1 on alloc).
4. `defrtrlist_update()` takes a **caller ref** before returning non-NULL (and
   `nd6_ra_input()` drops it after consuming `dr`).
5. Each `pfxrtr_add()` takes a **pfxrtr ref**; `pfxrtr_del()` and
   `prelist_remove()` drop it.
6. `defrtrlist_del()` drops the list ref via `rele` instead of `kfree`, so an
   outstanding caller/pfxrtr reference keeps the entry alive until the last ref
   is released.

This closes the race: even if `defrtrlist_del()` runs concurrently with
`pfxrtr_add()`, the entry cannot be freed while any caller or pfxrtr reference
is outstanding, so `pfr->router` can never dangle.

## Fix validation (Phase 8)

- **Baseline (`#0` with-src, unpatched):** the race →
  `Stopped at nd6_sysctl_prlist.part.10+0x165` UAF page-fault panic. ✅ reproduced.
- **Patched (`#1`, single-fix kernel built from this `fix.diff`):** under the
  identical workload, `nd6_sysctl_prlist` page fault **does not occur** (0
  matches in `boot.log`).  The guest either stays up or transiently wedges on
  mbuf exhaustion and recovers; it does **not** hit the DF-0417 UAF.
- A **separate** panic can still occur under a very heavy RA flood:
  `panic: rtrequest1_msghandler: rtrequest table req 1, failed on cpu5, error 55`
  (routing-table op fails `ENOBUFS` under mbuf exhaustion, kernel panics on the
  unexpected failure).  This is a **different bug**, present on both kernels,
  and is **not** addressed by this fix (out of scope for DF-0417).

`fix_status = fixed` for DF-0417.

## How to reproduce

```
ssh dfbsd                 # root (simulates the on-link attacker's machine)
cd /root/poc/DF-0417 && ./build.sh && ./run.sh
# expected on unpatched: guest panic in ~seconds; boot.log shows
#   Stopped at nd6_sysctl_prlist.part.10+0x165: movq 0x48(%rsi),%rdx
# expected on patched: no such panic (guest stays up / recovers).
```

Preconditions (realistic for an IPv6 host): `net.inet6.ip6.accept_rtadv=1`
(host mode), an on-link attacker, and any local read of the prefix list
(`ndp -p`, which any user can issue).  See `VERDICT.md` for full detail and
`run.log` / `panic.txt` / `fix_run.log` for the untrimmed evidence.
