# DF-3373 — connect(2)/listen(2) TOCTOU links one inpcb into two TCP hashes

## Root cause

The socket-layer guards are check-then-act with no token and no
handler-side re-check:

- `soconnect()` reads `SO_ACCEPTCONN` (uipc_socket.c:690) and
  `SS_ISCONNECTED|SS_ISCONNECTING` (:698) on the caller's thread.
- `solisten()` reads `SS_ISCONNECTED|SS_ISCONNECTING`
  (uipc_socket.c:332-333) and only then stores `SO_ACCEPTCONN` (:337)
  and queues the pru_listen message.
- With the default `kern.ipc.soconnect_async=1`, `SS_ISCONNECTING` is
  stored by `tcp_usr_preconnect` (tcp_usrreq.c:970), i.e. *after*
  `soconnect`'s checks and after a `kmalloc` — a real window on the
  connector thread.

Neither `tcp_usr_listen` (guards only `TF_LISTEN` and `inp_lport == 0`)
nor `tcp_usr_connect`/`tcp_connect` (guards only `inp_faddr != ANY`)
re-checks the opposite state. Both orders of the two serialized pru
messages therefore complete and the **same inpcb gets linked into both
the wildcard hash and the connection hash**:

- listen-msg first: pcb becomes listener (INP_WILDCARD,
  in_pcb.c:2283); `tcp_connect` then proceeds — reproduced panic at
  `in_pcbunlink_flags` ("already linked", in_pcb.c:311) via
  `tcp_connect`'s migration unlink (tcp_usrreq.c:1235); without
  INVARIANTS it would reach `in_pcbinsconnhash` (in_pcb.c:1905) and
  gain INP_CONNECTED while INP_WILDCARD.
- connect-msg first: pcb enters SYN_SENT (INP_CONNECTED,
  in_pcb.c:1906); `tcp_usr_listen` then migrates/inserts into the
  wildcard hash (tcp_usrreq.c:461/513, asserts :507 or in_pcb.c:2289).

## Production-kernel impact (why this is memory corruption, not just DoS)

`in_pcb_rmhash` (in_pcb.c:2381-2383) removes **only one** membership
(`if (INP_WILDCARD) ... else if (INP_CONNECTED) ...`). Teardown of the
dual-member pcb leaves the other hash bucket pointing at freed memory;
subsequent `in_pcblookup_hash` walks dereference the freed inpcb
(use-after-free on a 4-tuple the attacker controls). Additionally the
listening socket is simultaneously a connected 4-tuple, so incoming
connection attempts can be matched/accepted against the wrong state.

## Reproduction (guest: DragonFly 6.5-DEVELOPMENT #0, INVARIANTS)

`listen_connect_race.c`: thread A loops
{`socket()`; publish fd; `connect(10.0.2.2:1)` (nonblocking)}, thread B
spins `listen(fd, 1)`. Unprivileged (`maxx`, uid 1001).

**Reproduced twice from clean `with-src` snapshots, both within ≤ 45 s:**

```
panic: already linked
cpuid = 0
in_pcbunlink_flags() at in_pcbunlink_flags+0x5e
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; ssh session dies, `vm.sh status` → down.)

## Fix

`fix.diff` — handler-side, race-free state guards (pru messages are
serialized per socket):

- `tcp_usr_listen`/`tcp6_usr_listen`: reject when
  `tp->t_state != TCPS_CLOSED || (so->so_state &
  (SS_ISCONNECTED|SS_ISCONNECTING)) || inp->inp_fport != 0 ||
  inp->inp_pcbinfo == NULL` (last term catches a connect message that is
  mid-migration between netisrs).
- `tcp_connect`/`tcp6_connect`: reject when `TF_LISTEN` (covers both the
  first entry and the PRUC_RECONNECT re-entry after forwarding).

## Fix validation

Baseline panic reproduced twice on stock; after applying `fix.diff` to the
guest `/usr/src`, `make nativekernel KERNCONF=X86_64_GENERIC`, reboot, and
re-running the identical hammer (same duration, longer second run) the
guest survived with clean `EOPNOTSUPP`/`EINVAL` returns — see
`run_fixed.log`, `fix_results` in verdict.json.

## Build / run

```
cc -O2 -pthread -o listen_connect_race listen_connect_race.c
./listen_connect_race 45        # stock kernel: panics within seconds
```
