# DF-3298 — CARP softc use-after-free via deferred IP reassembly (dangling `m_pkthdr.rcvif`)

## What

`carp_proto_input()` can be made to execute on a **freed** `carp_softc`
(the carp(4) ifnet is embedded in the softc) after the carp interface has
been destroyed, because IP fragments queued for reassembly carry
`m_pkthdr.rcvif` pointing at the ifnet with no lifetime coupling to
interface teardown.

## Why (root cause)

1. `carp_input()` (sys/netinet/ip_carp.c:1219) claims frames for the
   CARP unicast MAC via `carp_forus()` and re-inputs them on the carp
   ifnet; `ether_reinput_oncpu()` (sys/net/if_ethersubr.c:1317-1321)
   stamps `m_pkthdr.rcvif = &sc->sc_if`.
2. An attacker-chosen IP-fragmented proto-112 datagram gets queued in
   the per-CPU reassembly queue (sys/netinet/ip_input.c:850-865,
   `ip_reass()`), keyed only by (id, src, dst, proto)
   (ip_input.c:1005-1008) — **not** by ifnet. The queue can outlive the
   carp interface by an attacker-chosen time (last fragment sent later).
3. `carp_clone_destroy()` (ip_carp.c:703-726) frees the softc/ifnet
   with `kfree(sc, M_CARP)` — nothing purges reassembly queues.
4. The final fragment arrives on the parent (tap0/vtnet0), completes
   reassembly — the **head fragment's** pkthdr (rcvif = freed carp ifnet)
   survives `m_cat()` (ip_input.c:1275-1287) — and the datagram is
   dispatched to `carp_proto_input()` which dereferences the freed
   ifnet/softc.

## Trigger (unauthenticated, L2-adjacent)

Fragment #1 sent to the CARP unicast MAC (00:00:5e:00:01:vhid — public,
it is the answered ARP MAC of the virtual IP) while carp0 is MASTER;
`ifconfig carp0 destroy` (normal admin churn) happens at any later time;
fragment #2 completes reassembly afterwards. No CARP key is needed to
reach the freed-memory dereferences (vhid/version/HMAC gates sit behind
them).

## Build / Run (guest, as root)

```
sh build.sh      # cc -O2 -o frag_send frag_send.c; setstate; mjoin
sh setup.sh      # tap0 10.0.0.1/24; carp0 vhid 1 pass pocpass; force MASTER
sh run.sh        # variant A (UAF read) + variant B (valid CARP header)
```

## Expected (stock kernel) — what "reproduced" looks like

```
M_CARP in-use after destroy: 0                      # softc IS freed
/dev/tap0: wrote 42 bytes (variant A2)              # post-destroy frag2
carpstats after A:  ... 1 packet received (IPv4)    # carp_proto_input ran
dmesg: carp_proto_input: packet received on stopped carp interface: carp0
                                                     # if_xname read from freed chunk
```

`vmstat -m | grep CARP` showing 0 allocations while the CARP input path
still runs and prints the dead interface's name is the decisive
evidence: the code is operating on freed M_CARP memory
(ip_carp.c:940-971 deref `ifp->if_type`, `ifp->if_flags`,
`ifp->if_softc`, `sc->sc_carpdev`, `cifp->if_xname`).

## Honest limits (why no uid=0)

The deeper write path (`sc->sc_init_counter = 0; sc->sc_counter =
tmp_counter;` at ip_carp.c:1150-1151 — 8 attacker-controlled bytes into
the freed 1.83K chunk) is currently gated: `carp_stop()`
(ip_carp.c:2826) clears IFF_RUNNING *in the same chunk* before the
free, so the stale image fails `CARP_IS_RUNNING()` (the "stopped"
branch observed in dmesg). Reaching the write requires the freed chunk
to be re-groomed with IFF_UP|IFF_RUNNING|IFT_CARP and a matching
vhid/HMAC-state — but the M_CARP zone only serves carp softcs, so
grooming requires another privileged `ifconfig carp create`. On today's
kernel the primitive is therefore a deterministic UAF **read** (flags
bytes + 16-byte stale `if_xname` disclosure to the console/log), plus a
latent UAF write that any future reordering of the teardown flags would
re-open. See VERDICT.md.

## Fix

`fix.diff` — purge all reassembly queues referencing the ifnet
(synchronously, on every netisr) from `carp_clone_destroy()` before
`kfree(sc)`. Validated on a patched kernel: post-destroy frag2 no
longer reaches carp_proto_input (see VERDICT.md, fix section).

Generic recommendation (out of this diff's scope): the same dangling
rcvif exists for *any* destroyed ifnet with queued fragments; the purge
belongs in `if_detach()` for all drivers.
