# VERDICT — DF-3415

**status: reproduced | impact: leak | confidence: certain**

## The finding

`in6_pcbconnect()` (`sys/netinet6/in6_pcb.c:337`) reads
`sin6->sin6_addr` at line 344 (`IN6_IS_ADDR_V4MAPPED`) **before** the
sockaddr is validated. The validation (`nam->sa_len != sizeof(*sin6)`,
`sin6->sin6_family != AF_INET6`) lives in `in6_pcbladdr()`
(`sys/netinet6/in6_pcb.c:282-285`) and is only reached at line 351, *after*
the address-field read.

`getsockaddr()` (`sys/kern/uipc_syscalls.c:1513-1532`) allocates
`kmalloc(len, M_SONAME)` with the exact user-supplied `len`
(`2 <= len`, `len <= SOCK_MAXADDRLEN`) and copies exactly `len` bytes.
Any `len < 28` therefore makes every read of `sin6_addr` (file offsets
8..23) an out-of-bounds read past the allocation — here up to 16 bytes
past a 2-byte allocation, or 3 bytes past a 17-byte one for the
`IN6_IS_ADDR_V4MAPPED` window (offsets 8..19, `sys/netinet6/in6.h:316-318`).

Live unprivileged path (no UDP6 `pru_preconnect` exists — only v4
`udp_preconnect`, `sys/netinet/udp_usrreq.c:1975`):

```
connect(2) → getsockaddr(kmalloc(len))      uipc_syscalls.c:1513
  → soconnect → so_pru_connect              uipc_socket.c:685
  → udp6_connect                            udp6_usrreq.c:583
      reads sin6_addr at udp6_usrreq.c:596  (same family, first reader)
  → in6_pcbconnect                          in6_pcb.c:337
      reads sin6_addr at in6_pcb.c:344      ← THIS FINDING
  → (only on false) in6_pcbladdr            in6_pcb.c:275
      validates sa_len == 28 at in6_pcb.c:282
```

## Reproduction (guest, DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC, uid 1001)

Slab mechanics that make the groom deterministic
(`sys/kern/kern_slaballoc.c`):

- `zoneindex()` (8-byte granularity below 128): sizes 17..24 all round to
  the **24-byte chunk zone** — groom `bind(24)` and trigger `connect(17)`
  share it.
- `_kfree()` local path writes only `chunk->c_Next` (bytes 0..7) and pushes
  the chunk on the per-zone LIFO list `z->z_LChunks`; `use_weird_array`
  (free-scrub) defaults to 0 — **bytes 8..23 survive the free**.
- Both `sys_bind` (`uipc_syscalls.c:177`) and `sys_connect` allocate/free
  the soname on the **syscall thread**, same CPU → LIFO reuse is immediate.

`IN6_IS_ADDR_V4MAPPED` needs bytes 8..19 == `00*8 || 00 00 ff ff`. The
17-byte trigger supplies bytes 0..16 (zeros); **bytes 17..19 are stale heap
bytes from the groom**. Results (3 independent runs × 20 rounds each,
100% stable):

| variant | stale bytes 17..19 | EADDRNOTAVAIL | EINVAL |
|---|---|---|---|
| A: groom `00 ff ff` | complete v4-mapped pattern | **20/20** | 0 |
| B: groom `00 00 00` | pattern broken | 0 | **20/20** |
| C: no groom | whatever the zone held | 0 | **20/20** |

A vs B differ **only** in heap bytes that lie outside the sockaddr handed
to `connect()`. The errno is therefore chosen by out-of-bounds heap reads
that happen before validation. Full logs: `run.log`, `run.2.log`.

## Attribution

Externally the errno cannot distinguish which of the two premature reads
decided (both read the same bytes: `udp6_usrreq.c:596` first, then
`in6_pcb.c:344`); the code trace pins both. The finding filed against this
file is the `in6_pcb.c:344` instance — it is unconditional in
`in6_pcbconnect()` and the only live caller (`udp6_usrreq.c:608`) feeds it
unvalidated lengths; `tcp_syncache.c:791` passes a kernel-built full-size
sockaddr. `in6_pcbbind()` is *not* affected (its `sa_len` check at
`in6_pcb.c:129` precedes the first field read at line 138).

## Impact ceiling (honest)

- The stale bytes never reach userspace; they gate two error returns
  (`EADDRNOTAVAIL` vs `EINVAL`) — a comparison oracle on 3 heap bytes per
  attempt (the predicate is an AND of byte equalities, so per-attempt
  leakage is sub-bit in practice).
- No escalation path: `in6_pcbladdr()` still rejects every short sockaddr,
  so no connection state is ever established from OOB data.
- Severity Low (kernleak), consistent with DF-3372 (Low); DF-3357 was
  Medium only because there the garbage address *became the packet
  destination*.

## Fix validation

`fix.diff` moves the `sa_len`/family validation ahead of the
`IN6_IS_ADDR_V4MAPPED` read (identical shape to the family fix; mirrors
the checks already at `in6_pcb.c:282-285` and the bind-side discipline at
`in6_pcb.c:129-135`). **Not rebuilt/tested in-guest:** Low-severity
out-of-bounds *read* whose observable effect is an errno choice; the
conditional-verification policy for this batch reserves the full
kernel-rebuild cycle for Critical/High or memcorrupt/privesc findings. The
patch is a pure reorder — after it, every connect() with `sa_len != 28`
returns `EINVAL` before any address field is read, killing the oracle by
construction.

## 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.
