# DF-3373 VERDICT — connect(2)/listen(2) TOCTOU → single inpcb in two TCP hashes

## Reproduced? YES — kernel panic, twice, unprivileged, ≤45 s per attempt

- Guest: DragonFly 6.5-DEVELOPMENT #0 (stock INVARIANTS `X86_64_GENERIC`,
  Jul 2 2026 build), 6 vCPUs, user `maxx` (uid 1001).
- PoC: `listen_connect_race.c` — thread A: fresh `socket()` + nonblocking
  `connect(10.0.2.2:1)` in a loop; thread B: `listen(fd,1)` spin.
- Baseline run 1: panic at ~≤45 s (ssh session died; `vm.sh status` → down).
  Baseline run 2 (after `vm.sh reset with-src`, fresh disk): identical panic.
  Signature both times:

```
panic: already linked
cpuid = 0
in_pcbunlink_flags() at in_pcbunlink_flags+0x5e
tcp_connect() at tcp_connect+0x56c
netmsg_service_loop() at netmsg_service_loop+0xe9
Debugger("panic")
```

  (`panic.txt`, `boot_panic.log`; serial console captured via `vm.sh log`.)

## Mechanism (proven on-guest, matches source)

`solisten` (uipc_socket.c:332) and `soconnect` (uipc_socket.c:690,698) guard
with plain check-then-act flag reads; with `kern.ipc.soconnect_async=1`
(default, verified) `SS_ISCONNECTING` is only stored by
`tcp_usr_preconnect` (tcp_usrreq.c:970) — after `soconnect`'s checks and
after a `kmalloc` in `so_pru_connect_async` (uipc_msg.c:214). A `listen(2)`
whose flag read slips into that window queues a pru_listen message that
`tcp_usr_listen` (guards: only `TF_LISTEN` and `inp_lport == 0`,
tcp_usrreq.c:481-488) happily processes against the racing connect.

Reproduced interleaving: listen message first → inpcb wildcard-hashed
(INP_WILDCARD, in_pcb.c:2290) → `tcp_connect`'s hashport migration
`in_pcbunlink` (tcp_usrreq.c:1235) → KASSERT `(inp_flags &
(INP_WILDCARD|INP_CONNECTED)) == 0` (in_pcb.c:311) → `panic: already linked`.

The mirrored interleaving (connect message first) is equally fatal:
`tcp_usr_listen` would hit `ASSERT_INP_NOTINHASH` (tcp_usrreq.c:507 /
in_pcb.c:2289) or `in_pcbunlink` again.

## Impact

- **This INVARIANTS kernel: reliable local unprivileged panic (DoS)** —
  hit within ≤45 s of hammering, twice from clean snapshots.
- **Production (non-INVARIANTS) kernels: kernel memory corruption.** The
  same interleaving links one inpcb into BOTH the wildcard hash (via
  `inpcontainer`s, in_pcb.c:2272-2274) and the connection hash
  (`inp_hash`, in_pcb.c:1913), setting INP_WILDCARD|INP_CONNECTED.
  `in_pcb_rmhash` (in_pcb.c:2381-2383) removes only ONE membership
  (wildcard wins), so teardown leaves the connhash bucket holding a
  pointer to the freed inpcb; any later packet matching that 4-tuple
  walks it (`in_pcblookup_hash`) → use-after-free with attacker-chosen
  tuple. Additionally the hybrid socket serves both listener and client
  lookups simultaneously. (Not demoable on this INVARIANTS guest: every
  dual-membership path asserts first — documented from source.)

## Exploit chain

unpriv user → 2-thread connect/listen race (≤45 s) → inpcb linked into
wildcard+conn hashes → INVARIANTS: panic / production: one-sided hash
removal at close → dangling connhash entry → in_pcblookup_hash UAF on
attacker-chosen 4-tuple (memory-corruption ceiling; not pursued to
uid=0 on this INVARIANTS guest).

## Fix validation (mandatory for reproduced memcorrupt) — FIXED

`fix.diff` (v3) adds race-free handler-side guards:

- `tcp_usr_listen`/`tcp6_usr_listen`: `TF_LISTEN` early-out (backlog
  update) first; then reject when `t_state != TCPS_CLOSED ||
  SS_ISCONNECTED|SS_ISCONNECTING || inp_fport != 0 || inp_pcbinfo == NULL`
  (PRUL_RELINK re-entry exempt), and **undo `solisten`'s SO_ACCEPTCONN**
  under `so_rcv.ssb_token` so `tcp_input` (tcp_input.c:950) and
  `soclose` never see a "listener" that isn't one.
- `tcp_connect`/`tcp6_connect`: reject `TF_LISTEN` with `EOPNOTSUPP`
  (covers first entry and PRUC_RECONNECT re-entry) without
  `soisdisconnected()` on the listener (children inherit parent state,
  uipc_socket2.c:383).

Two earlier fix revisions crashed under the same hammer and were
diagnosed and corrected (v1: guards but no SO_ACCEPTCONN undo → NULL
deref in `in_pcbdetach` via tcp_input's listener branch; v2: guard
ordered before the TF_LISTEN early-out → re-listen cleared
SO_ACCEPTCONN on a live listener). v3 survived.

- Baseline (stock): panic ≤ 45 s, twice — `panic.txt`.
- Patched kernel `#1: Fri Sep 11 17:25:03 UTC 2026`
  (`make nativekernel`/`installkernel` in guest, fix.diff applied to
  /usr/src): `run_fixed.log` — 377,194 connect iters / 45 s, 140,024
  EOPNOTSUPP rejections, 162,885 successful listens, **SURVIVED**;
  `run_fixed.2.log` — 1,024,176 iters / 120 s, **SURVIVED**. ssh (TCP)
  fully functional throughout.

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