# VERDICT — DF-3432

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

## The finding

`udp6_connect()` (`sys/netinet6/udp6_usrreq.c:581`) casts the connect(2)
sockaddr and evaluates `IN6_IS_ADDR_V4MAPPED(&sin6_p->sin6_addr)` at
**line 597 — before any `sa_len` validation**. The first length check on
this entire path lives deep inside `in6_pcbladdr()`
(`sys/netinet6/in6_pcb.c:282`), reached only via `in6_pcbconnect()` at
`udp6_usrreq.c:608`, i.e. *after* the premature read.

`getsockaddr()` (`sys/kern/uipc_syscalls.c:1513`) kmallocs exactly the
user-supplied length (`2 <= len <= 255`) and copies exactly `len` bytes.
For `len < 28` every read of `sin6_addr` (file offsets 8..23) is
out-of-bounds past the allocation — up to 16 bytes past a 2-byte
allocation; the `IN6_IS_ADDR_V4MAPPED` window is offsets 8..19
(`sys/netinet6/in6.h:316-318`).

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

```
connect(2) → getsockaddr(kmalloc(len))        uipc_syscalls.c:1513
  → soconnect → so_pru_connect                uipc_socket.c
  → udp6_connect                              udp6_usrreq.c:583
      reads sin6_addr at udp6_usrreq.c:597    ← THIS FINDING (first reader)
      true  → EADDRNOTAVAIL at :598, return   (in6_pcbconnect never runs)
      false → in6_pcbconnect at :608
                reads sin6_addr at in6_pcb.c:344   (DF-3415, other file)
              → in6_pcbladdr validates sa_len==28  in6_pcb.c:282
```

DF-3415's verification already noted this site ("same family, first
reader") but was filed against `in6_pcb.c`. This finding is the
`udp6_usrreq.c` instance — unconditional on every short-sockaddr
`connect(2)` to an AF_INET6 SOCK_DGRAM socket, and it decides the errno
*before* `in6_pcbconnect()` is ever called.

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

Slab mechanics (`sys/kern/kern_slaballoc.c`), identical to DF-3415's
verified groom:

- `zoneindex()`: sizes 17..24 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);
  free-scrub (`use_weird_array`) defaults 0 — **bytes 8..23 survive**.
- `sys_bind` and `sys_connect` both alloc/free the soname on the
  syscall thread, same CPU → immediate LIFO reuse.

`IN6_IS_ADDR_V4MAPPED` needs bytes 8..19 == `00*10 || 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,
100% stable):

| variant | stale bytes 17..19 | EADDRNOTAVAIL | EINVAL |
|---|---|---|---|
| A: groom `00 ff ff` | v4-mapped pattern completes | **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 lying outside the 17-byte sockaddr
handed to `connect()`. When A fires, the return happens at
`udp6_usrreq.c:598` — `in6_pcbconnect()` (and DF-3415's read) is never
reached, so the deciding read for variant A is the one at :597.

## Attribution (honest)

Both premature readers (`udp6_usrreq.c:597` here, `in6_pcb.c:344` in
DF-3415) read identical bytes with no writer in between, so the errno
oracle alone cannot separate them for the *false* branch (B falls
through to in6_pcbconnect). For the *true* branch (A) the short-circuit
at :598 means this finding's read is the one that decided, by code
trace. Both are real OOB reads on every short-sockaddr v6 UDP connect();
DF-3415 covers the `in6_pcb.c` instance, this covers `udp6_usrreq.c`.

## 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 (AND of byte equalities → limited per-attempt info).
- No connection state is ever established from OOB data: validation in
  `in6_pcbladdr()` still rejects every short sockaddr.
- Severity Low, kernleak family — consistent with DF-3372 (Low) and
  below DF-3357 (Medium: v4 twin reads garbage as the actual packet
  destination, since udp_send there lacks the pre-check that
  `udp6_send` has at `udp6_usrreq.c:686-693`).

## Fix

`fix.diff` — mirror `rip6_bind`/`rip6_connect`/`in6_pcbbind`: validate
`nam->sa_len == sizeof(struct sockaddr_in6)` at the top of
`udp6_connect()`, before the first field read. `git apply --check`
passes against the read-only `sys/` tree. Not rebuilt in-guest: Low
severity / leak-class (errno oracle), and this batch's conditional-verify
policy reserves the kernel rebuild/reboot cycle for Critical/High or
memcorrupt/privesc findings (same policy recorded for DF-3415). The
patch is a pure precondition insertion: with it, every short-sockaddr
connect() returns EINVAL before any address byte is read, eliminating
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.
