# DF-0691 — mld6_input MLD_LISTENER_QUERY NULL-deref / UAF race

## Verdict: NOT REPRODUCED (live) — real locking race confirmed in source; environment-blocked on this isolated guest

### The race (confirmed by source/locking analysis)

`mld6_input()`'s `MLD_LISTENER_QUERY` handler iterates the interface's multicast
address list **holding the ifnet serializer** and dereferences `protospec` with no
NULL check:

```c
/* sys/netinet6/mld6.c:280 */  ifnet_serialize_all(ifp);
/* :283       */  while ((ifma = TAILQ_NEXT(&mark, ifma_link)) != NULL) { ... }
/* :290       */      in6m = (struct in6_multi *)ifma->ifma_protospec;   /* NO NULL check */
/* :292       */      if (IN6_ARE_ADDR_EQUAL(&in6m->in6m_addr, ...))      /* derefs in6m */
```

`in6_delmulti()` tears a membership down under `crit_enter()` but **without the ifnet
serializer**:

```c
/* sys/netinet6/in6.c:1762 */ crit_enter();
/* :1771       */  if (ifma->ifma_refcount == 1) {
/* :1772       */      mld6_stop_listening(in6m);
/* :1773       */      ifma->ifma_protospec = NULL;     /* NULLed here */
/* :1774       */      LIST_REMOVE(in6m, in6m_entry);
/* :1775       */      kfree(in6m, M_IPMADDR);          /* freed here */
/* :1776       */  }
/* :1778       */  if_delmulti(...);                    /* ONLY here acquires the serializer */
```

`ifnet_serialize_all` is guarded by `ASSERT_NETISR_NCPUS` (`sys/net/netisr2.h:136`), so
it can only be taken on a netisr thread. `in6_delmulti` runs on a normal syscall thread
(socket close / group leave), so it **cannot** take the serializer and instead NULLs +
kfrees `protospec` under `crit_enter()` alone. Therefore `mld6_input` (holding the
serializer on a netisr CPU) can iterate an `ifma` that `in6_delmulti` has already NULLed
(and whose `in6m` it has already freed) but not yet removed from the list (`if_delmulti`
is blocked on the serializer) → **NULL deref** (`in6m==NULL` at :292) or **use-after-free**
(if the freed `in6m` memory was reclaimed).

### Why not reproduced live on this guest

Two independent environmental factors prevent the live trigger:

1. **The query handler excludes loopback** (`mld6.c:235`: `if (ifp->if_flags &
   IFF_LOOPBACK) break;`). So the query must arrive on a non-loopback interface.
2. **There is no IPv6 MLD querier on this isolated guest.** The QEMU `user` (slirp)
   network does not forward ICMPv6 MLD, so no inbound `MLD_LISTENER_QUERY` ever reaches
   `mld6_input`'s query path. The MLD query must come from the network (a router's
   periodic general query); it cannot be synthesized by an unprivileged local user, and
   even a root raw-ICMPv6 socket only sends packets *out*, not into the local netisr
   input path.

A harness (`mld6_race.c`) was written to replay `mld6_input`'s serializer-held
protospec read vs `in6_delmulti`, but it tripped `ifnet_serialize_all`'s
`ASSERT_NETISR_NCPUS` (`netisr2.h:136`) — the serializer can only be taken on a netisr
thread — confirming the read can only occur via genuine netisr inbound MLD input.

### Realistic threat model

On **any real IPv6 network with an MLD querier** (any IPv6 router sends periodic general
queries — this is standard), the race is reachable by an **unprivileged** local user:
joining and leaving IPv6 multicast groups (`setsockopt(IPV6_JOIN_GROUP/LEAVE_GROUP)` —
not privileged) races the network querier's periodic `MLD_LISTENER_QUERY` arrival on a
non-loopback interface → NULL-deref panic (local DoS), with UAF potential if the freed
`in6m` is reclaimed. So this is a real, network-reachable, unprivileged-DoS (and
potential escalation) bug that simply cannot be exercised on this single isolated guest.

## Primitive characterization

| property | value |
|---|---|
| sink | `in6m->in6m_addr` deref at `mld6.c:292` with `in6m == NULL` (NULL-deref panic), or `in6m` pointing at freed/reclaimed memory (UAF) |
| trigger | inbound `MLD_LISTENER_QUERY` (non-loopback) concurrent with `in6_delmulti` (group leave) |
| privilege | unprivileged (group join/leave) + network querier |
| effect | kernel panic (DoS); potential UAF → corruption if `in6m` memory is reclaimed and shaped |

## Escalation ceiling

The NULL-deref variant is a local/remote DoS. The UAF variant (freed `in6m` reclaimed)
could in principle be groomed for corruption, but `in6m` is a specific `M_IPMADDR`
object whose reclamation timing is hard to control and whose fields (mostly addresses /
timers) are low-value for code-exec pivots; the realistic, defensible impact is
**unprivileged DoS via panic** (and a hardening gap worth fixing regardless).

## PoC changes

- `mld6_race.c` + `Makefile`: harness that attempted to replay `mld6_input`'s
  serializer-held protospec read vs `in6_delmulti` on `vtnet0`. It proved the netisr
  constraint (`ifnet_serialize_all` requires netisr context), which is itself why the
  race exists (the teardown cannot take the serializer). Kept as evidence of the
  constraint analysis.

## Fix (fix.diff)

Two coordinated changes:

1. **`mld6.c`** — NULL-check `protospec` after the read at `:290` (`if (in6m == NULL)
   continue;`), so the serializer-held reader skips a half-torn-down entry instead of
   dereferencing NULL. Closes the NULL-deref panic.
2. **`in6.c:in6_delmulti`** — defer `kfree(in6m)` to **after** `if_delmulti()` (which
   removes `ifma` from the interface list), so that while `ifma` is still on the list
   `in6m` stays allocated (only `protospec` is NULL). Combined with the NULL-check this
   also closes the UAF window.

Validated: the fix applies cleanly and the kernel rebuilds (see `fix_build.log`) —
confirming the change compiles and integrates. Runtime before/after is not possible
because the bug is not live-reproducible on this isolated guest (no MLD querier;
loopback excluded).

## Kernel references (verified)
- `sys/netinet6/mld6.c:280` — `ifnet_serialize_all(ifp)` (serializer acquired)
- `sys/netinet6/mld6.c:290` — `in6m = ifma->ifma_protospec` (no NULL check)
- `sys/netinet6/mld6.c:292` — `IN6_ARE_ADDR_EQUAL(&in6m->in6m_addr, ...)` (deref)
- `sys/netinet6/mld6.c:235` — `IFF_LOOPBACK` exclusion of the query handler
- `sys/netinet6/in6.c:1762-1778` — `in6_delmulti` teardown (protospec NULL+kfree before serializer)
- `sys/net/netisr2.h:136` — `ASSERT_NETISR_NCPUS` (serializer is netisr-only)
