# DF-0355 — nd6_sysctl_drlist/prlist iterate global lists without nd6_mtx

## Verdict
**INCONCLUSIVE — code-level lock-order violation confirmed by source
trace; race not deterministically triggered in test window.**

The buggy sysctl handlers DO iterate the global `nd_defrouter` TAILQ
and `nd_prefix` list without holding `nd6_mtx`. The ioctl path that
walks the same lists DOES hold the lock. The contrast proves the
omission is a real bug, not a stylistic choice. However, on a
single-CPU race against RA processing the window is narrow; in our
limited stress test we did not win the race and did not observe a
UAF/panic. The bug remains real; the impact is theoretical here.

## Bug mechanism (source trace)
File: `sys/netinet6/nd6.c`.

### The lock IS taken on the ioctl path:
- Line 1441: `mtx_lock(&nd6_mtx);` — guarding `SIOCGDRLST_IN6`
  iteration of `nd_defrouter` (lines 1442–1460).
- Line 1473: `mtx_lock(&nd6_mtx);` — guarding `SIOCGPRLST_IN6`
  iteration of `nd_prefix` (lines 1474–1513).
- Both release `mtx_unlock(&nd6_mtx)` at lines 1461/1514.

### The lock is NOT taken on the sysctl path:
- `nd6_sysctl_drlist` (2156–2195): walks `TAILQ_FIRST(&nd_defrouter)`
  → `TAILQ_NEXT(dr, dr_entry)` (lines 2168–2169) with **no mtx**.
- `nd6_sysctl_prlist` (2197–2267): walks `nd_prefix.lh_first` →
  `pr->ndpr_next` (line 2209) and per-prefix `pr->ndpr_advrtrs.lh_first`
  → `pfr->pfr_next` (lines 2238–2239) with **no mtx**.

### The lock IS taken by RA processing (the modifier):
- `defrtrlist_update` (`sys/netinet6/nd6_rtr.c:666`): `mtx_lock(&nd6_mtx)`,
  manipulates the `nd_defrouter` TAILQ (TAILQ_INSERT_TAIL line 702).
- `defrtrlist_del`, `prelist_remove`, `pfxrtr_add`, `pfxrtr_del`
  (nd6_rtr.c) all run under `nd6_mtx`.

### Race
On a multi-CPU system, the sysctl handler can be deep in
`TAILQ_NEXT(dr, dr_entry)` on CPU A when CPU B (netisr0) processes an
RA that calls `defrtrlist_del(dr)` — freeing the very `dr` whose
`dr_entry` the sysctl handler is about to dereference. UAF read,
panic, or info leak (depending on slab reuse).

## Reproduction attempt on this guest
We populated the kernel state with DF-0354's RA injector (60 routers +
1 prefix), then concurrently ran 10 unprivileged sysctl reads while
the injector kept flapping RAs. The guest stayed up across all runs;
no panic, no garbled output observed.

```
race run (60 RAs injected concurrently with 10 sysctl reads):
  guest still up after 10 reads  ✓ (no UAF triggered)
```

This is the expected outcome for a hard-to-win race on a mostly-idle
guest; it does NOT refute the bug. With a busier RA stream / longer
runtime / more CPUs, a UAF is achievable.

## Impact (theoretical, code-confirmed)
- **UAF read** of a freed `nd_defrouter` or `nd_prefix`/`nd_pfxrouter`.
- Outcome depends on slab reuse: most likely a panic (dereferencing
  freed `dr->ifp` etc.); potentially an info leak if the slab slot is
  reused with attacker-influenced data.
- Realistic trigger: any user reads `nd6_drlist`/`nd6_prlist` sysctl
  during a link where RAs are arriving — a default-IPv6 network under
  RA flood, or simply during routine RA refresh.

## Recommended fix
Hold `nd6_mtx` across the iteration in both sysctl handlers, mirroring
the ioctl path. The handlers already do per-entry work that is safe
under the mutex (the in6_recoverscope and SYSCTL_OUT copies happen
into a stack buffer; only the list walk and field reads need
protection). See `fix.diff`.

## Files in this folder
- `fix.diff`     — wrap both sysctl iterations in mtx_lock/unlock(nd6_mtx)
- `VERDICT.md`   — this file
- `manifest.json`
- `env.txt`      — guest environment
- `race_log.txt` — concurrent injector + sysctl-read test (no crash)
