# DF-3319 — v4 SIOCDLIFADDR kernel stack buffer overflow (in_lifaddr_ioctl)

**Status: REPRODUCED** (Fatal trap 12, control-flow corruption, guest wedged in
DDB) **+ fix validated** (patched kernel returns EADDRNOTAVAIL, guest stays up).

## What

`in_lifaddr_ioctl()` (`sys/netinet/in.c`) — the AF_INET `SIOCDLIFADDR` path —
copies ifaddr sockaddrs into a **64-byte on-stack** `struct in_aliasreq ifra`
using `sin_len` fields read out of the selected `in_ifaddr` as bcopy lengths:

```c
bcopy(&ia->ia_addr,     &ifra.ifra_addr,    ia->ia_addr.sin_len);     /* :949 */
bcopy(&ia->ia_dstaddr,  &ifra.ifra_dstaddr, ia->ia_dstaddr.sin_len);  /* :952 */
bcopy(&ia->ia_sockmask, &ifra.ifra_dstaddr, ia->ia_sockmask.sin_len); /* :955 */
```

None of these lengths is validated, and the `ia` is selected by the DF-0490
type confusion (`:911` filters `!= AF_INET6` in the IPv4 handler, `:923` casts
an `in6_ifaddr` to `in_ifaddr`). Under the confusion, `ia_sockmask.sin_len`
reads `in6_ifaddr` offset 344 = `ia_prefixmask.sin6_addr` **byte 12**, which is
`0xff` for **any IPv6 prefix length ≥ /104 — including the stock /128 of
lo0's ::1** — producing a deterministic **255-byte bcopy into the 64-byte stack
object (223 bytes past its end)** that smashes the return path of
`in_lifaddr_ioctl()` (running on the netisr0 dispatch thread, in.c:230-248).

DF-0490 claimed only the *unprivileged read* side (`:927`); this is the missed
*privileged write/stack-smash* side of the same confusion, plus the DF-3318
twin persistence path (root can poison `ia_dstaddr`/`ia_sockmask.sin_len`
directly via SIOCSIFDSTADDR/SIOCAIFADDR at `:602/:668`, which becomes the
live route into `:949-956` the moment the `:911` filter is fixed without
length validation).

## How to reproduce

```
cc -O -o smash smash.c && cc -O -o getlif getlif.c   # build.sh
./getlif lo0     # stock kernel: garbage (type confusion visible)
./smash lo0      # as root; Fatal trap 12 within ~1s; guest wedges in DDB
```

The query address `0.248.255.255` is derived in smash.c: the walk's candidate
expression (`:915-916`) reads through `&ifa->ifa_addr` — the address of the
*pointer field* — so `candidate` is always the high 4 bytes of the kernel
pointer (0xfffff800...) for every ifaddr; `00 f8 ff ff` as network bytes is
`0.248.255.255`, matching the first AF_INET6 ifaddr on lo0 (::1, /128).

## Expected (stock INVARIANTS kernel #0)

```
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xe4     (supervisor write data, page not present)
instruction pointer   = 0x8:0xfffff8004f308f46   <- heap address: smashed
frame pointer         = 0xfffff8004f308f28          return address jumped
current process       = Idle                        (netisr0 dispatch)
Stopped at -0x7ffb0cf70ba: addb %al,(%rax)          <- executing heap garbage
```
See `panic.txt` (serial console) — ssh dies; `vm.sh status` => down.

## Fix validation (`fix.diff`, kernel #3 `Thu Sep 10 19:51:37 UTC 2026`)

| check | stock #0 | patched #3 |
|---|---|---|
| `./smash lo0` | Fatal trap 12, DDB wedge | `EADDRNOTAVAIL`, guest up |
| lo0 after smash | (wedged) | 127.0.0.1 intact (no bogus delete) |
| `./getlif lo0` | `family=0 len=0` (confused) | `127.0.0.1 len=16 /8` — **v4 SIOCGLIFADDR now works** |
| normal alias add/del | n/a | works (no regression) |

The accidental observation on intermediate kernel #2 (clamps only, candidate
hunk missing) is recorded in `fix_run.log` history: no panic, but the
pointer-derived match still deleted 127.0.0.1 — evidence that the full fix
(filter + candidate deref + clamps + set-side validation) is required.
