# DF-3320 — IPv4 sin_len persisted verbatim into in_ifaddr → unprivileged kernel heap leak (SIOCGIFCONF / route sockets)

**Status: REPRODUCED** (unprivileged disclosure of adjacent-slab kernel heap
pointers, 3/3 identical runs) **+ fix validated** (setup rejected EINVAL,
leak gone, no regression).

## What

The IPv4 address-set ioctls persist user-supplied `sin_len` **verbatim** into
the live `struct in_ifaddr` (a 360-byte heap object with `ia_addr` at offset
312) and export it as `ifa->ifa_addr`:

* `in_ifinit()`: `ia->ia_addr = *sin;` — `sys/netinet/in.c:998`
  (SIOCSIFADDR / SIOCAIFADDR, privileged)
* `SIOCSIFDSTADDR`: `ia->ia_dstaddr = *(struct sockaddr_in *)&ifr->ifr_dstaddr;`
  — `in.c:602` (also becomes `ifa_dstaddr`)
* `SIOCAIFADDR` mask: `ia->ia_sockmask = ifra->ifra_mask;` — `in.c:668`
* `SIOCSIFBRDADDR`: `ia->ia_broadaddr = *(struct sockaddr_in *)&ifr->ifr_broadaddr;`
  — `in.c:625`; and for loopback `ia_dstaddr = ia_addr` — `in.c:1062-1063`

There is **no `sin_len` validation anywhere on these paths** (the in6 twin was
DF-3318). Unprivileged consumers then trust `sa_len` as a copy length:

* **`ifconf()`** (`SIOCGIFCONF`, no privilege check — `sys/net/if.c:1996-1997`,
  used by `ifconfig -a`):
  ```c
  copyout(sa, &ifrp->ifr_addr, sa->sa_len);   /* sys/net/if.c:2590-2592 */
  ```
* **route sockets** (`RTM_NEWADDR`/`RTM_DELADDR`/dump messages):
  `dlen = RT_ROUNDUP(sa->sa_len)` copies of `ifa_addr`/`ifa_dstaddr`/
  `ifa_netmask` — `sys/net/rtsock.c:1140,1175,1202` (readable unprivileged).

With `sin_len = 200`, SIOCGIFCONF copies 200 bytes starting at `&ia->ia_addr`
(object offset 312) — **152 bytes past the end of the allocation** — straight
to userspace.

This is the exact IPv4 twin of DF-3318's model: one privileged
misconfiguration (root, or a buggy config/routing tool feeding root an
unpadded sockaddr) plants the length; **every local user** can then read
adjacent kernel heap memory.

## How to reproduce

```
cc -O -o setup setup.c && cc -O -o leak leak.c    # build.sh
./setup lo0                                       # as root (one time)
su -m nobody -c "./leak 127.200.0.200"            # unprivileged, repeat 3x
```

## Expected (stock INVARIANTS kernel #0)

```
entry: lo0      sa_len=200 family=2 addr=127.200.0.200
  0: c8 02 00 00 7f c8 00 c8 ...   ia_addr      (poisoned len 200)
 16: c8 02 00 00 7f c8 00 c8 ...   ia_dstaddr   (loopback copy, len 200)
 32: 05 02 00 00 ff 00 00 00 ...   ia_sockmask  (/8, trimmed len 5)
 48: ..................            <- object ends here (360 = 312+48)
 72: d8 79 2e 4f 00 f8 ff ff       0xfffff8004f2e79d8  \
 80: e8 79 2e 4f 00 f8 ff ff       0xfffff8004f2e79e8   > adjacent
 88: f8 79 2e 4f 00 f8 ff ff       0xfffff8004f2e79f8  /  in_ifaddr's
                                     ifa_addr/ifa_dstaddr/ifa_netmask
```
Three kernel heap pointers (the next in_ifaddr in the 384-byte bucket —
added as a filler alias by setup), **identical across 3 consecutive runs**
(`run.log`, `leak_sample.txt`). Baseline before setup: no such entry.

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

* stock #0: setup OK → leak shows 200-byte entry with heap pointers.
* patched #3: `./setup lo0` ⇒ `SIOCAIFADDR(lo0, len=200): Invalid argument`
  (rejected at the door); unpriv leak ⇒ no entry; a normal
  `ifconfig lo0 alias 127.200.0.200/8` still succeeds and can be removed —
  no regression (`fix_run.log`).

See DF-3319's pack for the shared `fix.diff` (set-side validation is the
DF-3320 half; `in_ifinit()` and the SIOCAIFADDR/SIOCSIFDSTADDR/SIOCSIFBRDADDR
guards).
