# DF-0630 — PoC: udp6_ctlinput netisr deadlock

Remote unauthenticated single-packet permanent netisr wedge via crafted
ICMPv6 Destination Unreachable whose quoted UDP header is too short.

**Status: REPRODUCED on unpatched `#0`; fix VALIDATED on single-fix `#1`.**
See `VERDICT.md` for the full analysis.

## Files

- `trigger.c` — **verified in-guest injector** (raw `IPPROTO_ICMPV6` socket,
  root). Sends the crafted ICMPv6 DestUnreach (inner IPv6 `nh=UDP`, `plen=0`)
  to `::1`; the guest's own `icmp6_input` dispatches `udp6_ctlinput`, which
  `return;`s at `udp6_usrreq.c:435` without `lwkt_replymsg`, wedging the
  netisr that ran icmp6 input (self-deadlock on cpu0 via `cpu0_ctlport`).
- `udp6_ctlinput_deadlock.py` — original scapy trigger, for an **external
  attacker** with IPv6 reachability to the victim (the QEMU user-mode net is
  IPv4-only NAT, so the C injector is used in-guest instead).
- `build.sh` / `run.sh` — exact repro scripts.
- `fix.diff` — the one-line verified fix (`return;` → `goto out;`).
- `run.log` / `fix_run.log` / `fix_build.log` / `env.txt` — full evidence.

## Build & run (in-guest, as root)

```
./build.sh                       # cc -o trigger trigger.c
./run.sh                         # trigger ::1 1 ; then timed ping6/ssh probes
./run.sh 12                      # 12-shot burst
```

`run.sh` needs root (raw ICMPv6 socket). The root requirement is an artifact of
in-guest injection; the live vulnerability is remotely triggerable by any
unauthenticated host that can deliver an IPv6 packet to the victim.

## Expected outcome

### Unpatched kernel (`6.5-DEVELOPMENT #0`) — BUG PRESENT
Immediately after sending **one** packet:
- `ping6 ::1` hangs forever (8 s timeout, rc 124).
- ssh round-trip hangs (rc 124); `vm.sh status` ⇒ **down**.
- serial console still shows the live login prompt — **no panic, no `fatal
  trap`, no `ddb>`** (it is a permanent netisr wedge, not a crash).
- The box must be rebooted (`vm.sh reset`) to recover.

### Patched kernel (`6.5-DEVELOPMENT #1`, fix applied) — BUG GONE
After the same trigger (single-shot ×5 and a 12-shot burst):
- `ping6 ::1` replies in < 2 ms, 0% loss.
- ssh responsive; `vm.sh status` ⇒ **up**.

## Attack vector

- **Remote unauthenticated** — any host that can deliver an IPv6 packet to a
  victim address (global, or link-local for on-link attackers).
- IPv6 enabled by default; ICMPv6 cannot be broadly blocked without breaking
  NDP/IPv6.
- No prior UDP traffic and no credentials required — `icmp6_notify_error`
  derives the upper-layer protocol purely from the quoted inner header
  (`icmp6.c:874`).
- One packet wedges one netisr CPU permanently; ~N packets (one per CPU,
  varied source so RSS spreads them) freeze the entire network stack.

## Fix

One line at `sys/netinet6/udp6_usrreq.c:435`:

```diff
-			return;
+			goto out;
```

Routes the early exit through the existing `out:` label so the mandatory
`lwkt_replymsg(&msg->ctlinput.base.lmsg, 0)` at `:449` always runs. Matches the
finding markdown's recommended fix exactly.
