# DF-3389 — deferred-transport UAF read of `ip6a_dstia6`

**File:** `sys/netinet6/ip6_input.c:490-803` (+ consumers in
`sys/netinet/tcp_input.c:624-629,1069-1076`)
**Claim:** `ip6_setdstifaddr()` stores a raw `in6_ifaddr *` into the
packet's `PACKET_TAG_IPV6_INPUT` m_tag with no `IFAREF`; DragonFly defers
TCP/UDP processing via `lwkt_sendmsg(netisr_cpuport(0))`, so `tcp6_input`'s
`ia6->ia6_flags` dereference happens after the packet has sat in the netisr
queue; if the address is purged in that window the deferred read is a
use-after-free read.

## Verdict in one line

**REPRODUCED (natural race, poison-canary-confirmed, 4 hits in 2
independent 300 s full-rate runs; 0 hits in 18M-exposure controls).**
Impact ceiling: read of freed kernel heap memory driving a branch
(spurious anycast ICMPv6 unreachable / deprecated-address RST), panic
potential under slab reuse / INVARIANTS-adjacent container states. No
write primitive, no data exfiltration path — not an escalation, a
reliability/info-corruption bug. Severity Medium (as filed) is right.

## How to reproduce (canary kernel)

```
# guest, as root:
sysctl net.inet6.ip6.dad_count=0
sh /root/df3389/race.sh 300 3            # unpriv flood + root cycling
sysctl net.inet6.ip6.df3389_hits          # > 0 on hit
dmesg | grep "DF3389: UAF-HIT"
```

Expected (observed run 1): `df3389_hits: 3`, dmesg shows e.g.

```
DF3389: UAF-HIT ia6=0xfffff8004f31e000 ia6_flags=0xdf3389a6 (freed in6_ifaddr dereferenced by deferred transport6)
```

Controls (both must stay at 0 hits):
```
ifconfig lo0 inet6 fd99:3389::1/64        # static address
sh /root/df3389/race.sh 240 3 control     # 18,044,945 captures, 0 hits
```

## Components

| file | role |
|---|---|
| `cycler.c` | root: SIOCAIFADDR_IN6/SIOCDIFADDR_IN6 add/delete loop on lo0 (~2.3-2.7k cycles/s full rate) |
| `flooder.c` | unpriv (maxx): batched nonblocking TCPv6 connects to [fd99:3389::1]:33890 — SYNs through lo0 |
| `stretch.c` | unpriv (maxx): UDPv6 flood to the raced address (same tag-store + defer path; RFC3542 HBH/DSTOPTS attach is EPERM for unpriv on DragonFly, so the ext-header window-stretcher is not available unprivileged) |
| `race.sh` | orchestrator (race / control modes) |
| `df3389_canary.diff` | kernel instrumentation: poison+record freed in6_ifaddr in `ifac_free()`, check in `transport6_processing_handler()` |
| `fix.diff` | the actual fix: snapshot `ia6_flags` into the tag instead of storing the pointer |
| `logs/` | full untrimmed evidence (see below) |

## Evidence map

| file | what it shows |
|---|---|
| `logs/run1-race-output.txt` | 300 s race, 683,168 purge cycles, 1.73 M SYNs, 4.82 M UDP: **3 UAF-HITs**, counters 3/1523/1523 |
| `logs/run3-dmesg.txt` | second full-rate race (fresh boot): **1 UAF-HIT** at `ia6=0xfffff8004f31bde0` |
| `logs/run3-floods.txt` | flood totals for run 3 (824,193 cycles, 1.77 M SYNs, 5.59 M UDP) |
| `logs/control-staticaddr.txt` | static-address control: 18,044,945 captures/defers, **0 hits** |
| `logs/stock-nd6-panic-unrelated.txt` | UNRELATED nd6_timer panic (`rt_llinfo(...) != ln(...)`) triggered by full-rate cycling on the STOCK kernel — separate bug, not DF-3389 |
| `logs/canary-run1-and-nd6-panic2.txt` | serial console: run-1 UAF-HITs + the same unrelated nd6 panic |
| `logs/fix-validation.txt` | fixed kernel: build, functional sanity, 300 s full-rate race — no hits (by construction: pointer no longer exists), guest stays up, IPv6 TCP functional |
| `env.txt` | guest kernel/CPU/sysctl state |

## Fix

`fix.diff` replaces the borrowed pointer in `struct ip6aux` with a
`u_int32_t ip6a_dstia6_flags` snapshot taken at `ip6_setdstifaddr()` time
(while `ia6` is provably valid inside `ip6_input`), and switches the two
consumers (`tcp6_input` anycast check, `tcp_input` deprecated check) plus
the `ip6_input` boolean probe to the snapshot. Semantics are preserved
(the checks test the address state for *this* packet), and the UAF class
is eliminated outright: no `in6_ifaddr *` outlives `ip6_input()` anymore.

## Unrelated bug observed during verification

Full-rate `SIOCAIFADDR_IN6`/`SIOCDIFADDR_IN6` cycling (~3 k/s) reliably
panics the stock INVARIANTS kernel within ~60-120 s in
`nd6_timer_dispatch()` with `rt_llinfo(...) != ln(...)` (see
`logs/stock-nd6-panic-unrelated.txt`, second occurrence in
`logs/canary-run1-and-nd6-panic2.txt` with `rt_llinfo=0x7fffffffffffffff`).
That is a *different* defect (nd6 llinfo/route teardown race); it needs
its own finding. It is root-triggered (add/delete ioctls) but caps the
duration of any purge-race harness on stock kernels.
