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

**Status: REPRODUCED** (natural race, poison-canary-confirmed, unpriv-user
traffic racing a root-driven address purge) — impact ceiling is a
branch-on-freed-heap-memory read (connection-level misbehavior, panic
potential), **not** an escalation primitive. Medium severity as filed.

## 1. What was claimed

`sys/netinet6/ip6_input.c:796-805` (`ip6_setdstifaddr`) stores the raw
`struct in6_ifaddr *ia6` into the `PACKET_TAG_IPV6_INPUT` m_tag with no
`IFAREF` — a borrow from either the global cached route
(`ip6_input.c:484-490`, `ip6_forward_rt.ro_rt->rt_ifa`) or
`in6_ifawithifp()` (`ip6_input.c:534`, `in6.c:2319-2353` — walks
`if_addrheads[mycpuid]` and returns the match without a reference).
Terminal-header packets are then deferred: `ip6_input.c:739-755` does
`lwkt_sendmsg(netisr_cpuport(0), ...)` → `transport6_processing_handler`
(`ip6_input.c:776-790`) → `tcp6_input`, which dereferences the stored
pointer at `sys/netinet/tcp_input.c:624-625`
(`ia6->ia6_flags & IN6_IFF_ANYCAST`) and again at
`sys/netinet/tcp_input.c:1069-1076`
(`IN6_IFF_DEPRECATED`). If the address is purged while the packet sits
in the netisr queue, that dereference reads freed memory.

## 2. What was proven on the guest

### Instrumentation (df3389_canary.diff — audit-only, never for upstream)
`ifac_free()` (`sys/net/if.c:3484-3490`), immediately before the final
`kfree(ifa)`, poisons `((struct in6_ifaddr *)ifa)->ia6_flags = 0xDF3389A6`
and records the pointer in a 2048-slot ring.
`transport6_processing_handler()` reads every deferred packet's tag and,
when the pointer is ring-confirmed AND still carries the poison value,
prints `DF3389: UAF-HIT ...` and bumps `net.inet6.ip6.df3389_hits`.
The poison-check design makes false positives negligible: a *new*
`in6_ifaddr` is allocated `M_ZERO`, and unrelated reuse of the slab
overwrites the field (both verified reasoning paths).

### Race harness (unpriv traffic + root purge)
- `cycler.c` (root): `SIOCAIFADDR_IN6`/`SIOCDIFADDR_IN6` on
  `fd99:3389::1/64` @ lo0, ~2.3-2.7k full add/delete cycles per second.
- `flooder.c` (maxx, uid 1001): batched nonblocking TCPv6 connects to
  `[fd99:3389::1]:33890` — every SYN is a packet whose tag gets the
  borrowed pointer and whose `tcp6_input` run performs the claimed deref.
- `stretch.c` (maxx): UDPv6 to the same address — same tag-store and
  defer path (canary check sits before `pr_input`, so it covers UDP too).
- `net.inet6.ip6.dad_count=0` so re-added addresses skip DAD (tentative
  addresses are dropped at `ip6_input.c:496` and would not exercise TCP).

### Results (canary kernel, INVARIANTS, 6 CPU)

| run | mode | cycles | SYNs | UDP | captures/defers | UAF-HITs |
|---|---|---|---|---|---|---|
| 1 | race, full rate, 300 s | 683,168 | 1.73 M | 4.82 M | 1,523 | **3** |
| 2 | race, 508 c/s, 300 s | 152,514 | 2.69 M | 13.4 M | (cumulative) | 0 |
| 3 | race, full rate, 300 s (fresh boot) | 824,193 | 1.77 M | 5.59 M | (cumulative) | **1** |
| C1 | control, no address | 0 | ~0.2 M | 4.0 M | 1 | 0 |
| C2 | control, static address, 240 s | 0 | 2.28 M | 15.7 M | **18,044,945** | **0** |

The hits, e.g.
`DF3389: UAF-HIT ia6=0xfffff8004f31e000 ia6_flags=0xdf3389a6`,
are direct proof that the exact `ia6->ia6_flags` load the finding cites
executed on an `in6_ifaddr` that `ifac_free()` had already `kfree`'d
(poison still present ⇒ slab not yet reused ⇒ zero false-positive risk
for these events). Four confirmed events across two independent runs;
18M-exposure static-address control produced zero — the purge is the
necessary ingredient, exactly as claimed. Hit rate at full-rate cycling:
~1-3 per 300 s run (~1e-6..2e-6 per capture in the flap regime where
only ~1.5k packets/300 s get captures — the flap invalidates the route
cache for most SYNs, which is why captures collapse at high cycle rates).

### Why the window exists (mechanics)
The store happens in `ip6_input` while the borrowed `ia6` is covered by
(1) the per-CPU `ifa_containers[]` initial reference (held until
`ifa_destroy()`'s netmsg round reaches that CPU — `if.c:3571-3590`,
`netisr2.h:157-212`) and/or (2) the route's `IFAREF`
(`route.c:935`; dropped in `rtfree()` → `IFAFREE(rt->rt_ifa)` at
`route.c:299-306` when the last route ref goes away — including when the
stale global `ip6_forward_rt` cache is flushed at `ip6_input.c:427-431`
by the *next* packet after the route delete). Under full-rate flapping
the purge rounds, the route-cache invalidations and the flood-induced
netisr-0 queue backlog interleave such that the final `ifac_free()`
(completing on some CPU while the transport netmsg still sits behind
queue padding on netisr-0) lands before the deferred `tcp6_input` read.
The empirical hits supersede the initial FIFO-ordering intuition that
suggested safety: queue-padding messages enqueued between the capture
and the defer, plus route-ref drops outside netisr-0, break that
ordering. (A deterministic single-shot stall knob is impossible here:
stalling netisr-0 also stalls the synchronous `ifa_destroy` round
through CPU 0, which keeps the address alive — the bug genuinely needs
the multi-actor race, which the harness reproduces naturally.)

## 3. Impact assessment (honest ceiling)

- The primitive is a **read** of freed heap memory that drives two
  branches: ANYCAST → `icmp6_error` instead of TCP delivery, and
  DEPRECATED → RST/drop of the SYN. Observed effect in the wild:
  sporadic wrong-branch decisions on racing connections (silent, or
  visible as unexplained resets/unreachables during address churn).
- Slab reuse can make the load read attacker-influenced garbage; under
  INVARIANTS-adjacent states (e.g. the dead-container `IFAFREE` path
  this race can neighbor) this class manifests as a panic. No panic
  from *this* deref was observed on this guest within budget.
- No write primitive, no pointer/value flows to userspace ⇒ no path to
  `uid=0` from this bug alone. DoS/panic ceiling under reuse, info-class
  otherwise. Medium severity confirmed; bucket memcorrupt (read flavor).

## 4. Fix (fix.diff — validated)

Replace the borrowed pointer in `struct ip6aux` with a flags snapshot:
`u_int32_t ip6a_dstia6_flags` written in `ip6_setdstifaddr()` (while
`ia6` is valid inside `ip6_input`), read by the three consumers
(`ip6_input.c:531` presence probe → `ip6_findaux()`,
`tcp_input.c:624` ANYCAST, `tcp_input.c:1071` DEPRECATED via new
`ip6_getdstifaddr_flags()`). Semantics identical (the checks want the
address state for the packet being input); the UAF class is removed
outright — no `in6_ifaddr *` outlives `ip6_input()`.

Validation (logs/fix-validation.txt): fixed kernel (fix.diff on clean
tree, 0 build errors) survived the identical full-rate race twice
(505k and 824k-cycle-equivalent flood shapes; 1.06 M + 1.8 M SYNs) with
zero panics/anomalies, guest up throughout, `ping6 ::1` and TCPv6
SYN/RST roundtrips normal.

## 5. Incidental finding (NOT DF-3389)

Full-rate `SIOCDIFADDR_IN6` cycling (~3k/s) panics stock INVARIANTS
kernels in `nd6_timer_dispatch()` — `rt_llinfo(...) != ln(...)`, second
occurrence with `rt_llinfo=0x7fffffffffffffff` (garbage). Separate nd6
llinfo/route teardown race; evidence in
`logs/stock-nd6-panic-unrelated.txt`, `logs/canary-run1-and-nd6-panic2.txt`.
Root-triggered; deserves its own finding.

## 6. Conclusion

DF-3389 is a **real, reproducible use-after-free read** exactly along
the claimed path: uncounted `in6_ifaddr *` in the packet m_tag surviving
across the `lwkt_sendmsg` defer until `tcp6_input`'s `ia6->ia6_flags`
load, with the address purged in between. Verified with a poison canary
(4 hits / 2 runs), clean controls (0/18M), a build-validated one-hunk
fix that eliminates the dangling pointer, and an honest impact ceiling
of misbehavior/panic-class (no escalation path).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
