# DF-2694 — sorflush() vs sorecvtcp()/soreceive() receive-buffer UAF race

> **2026-10-16 weaponization update**: escalated beyond the panic to a
> **repeatable unprivileged arbitrary kernel-memory read** on the stock
> kernel.  See `README-weaponization.md`, `df2694_leak.c`,
> `leak_run1.bin`/`leak_run2.bin`, and the WEAPONIZATION section of
> `VERDICT.md`.

## What

`shutdown(fd, SHUT_RD)` racing `recv()` on a socket whose receive buffer is
being copied into *sleeping* user page faults frees the receive mbufs under
the copier:

* `sys/kern/uipc_socket.c:1953-1957` — `soshutdown()` deliberately does NOT
  take `ssb_lock` before `sorflush()`.
* `sys/kern/uipc_socket.c:1964-1991` — `sorflush()` takes only the receive
  token, snapshots the sockbuf, `bzero()`s it and frees every mbuf via
  `ssb_release() -> sbflush() -> sbdrop() -> m_freem()`
  (`sys/kern/uipc_socket2.c:754-760`, `sys/kern/uipc_sockbuf.c:451-508`).
* A DFly thread that **blocks** temporarily releases all its lwkt tokens
  (`lwkt_switch() -> lwkt_relalltokens()`, `sys/kern/lwkt_thread.c`).
  `sorecvtcp()` releases the token itself around its copy loop
  (`uipc_socket.c:1756/1758`) and marks the mbufs `M_SOLOCKED`
  (`uipc_socket.c:1746-1753`); generic `soreceive()` holds the token across
  its loop (`uipc_socket.c:1322`). Either way, the moment the copier sleeps
  inside `uiomove()` (page fault on a non-resident/swapped page) its token
  protection evaporates and `sorflush()` can free the chain being read.
* Nothing on the free path checks `M_SOLOCKED` (only `sbcompress()` does,
  `sys/kern/uipc_sockbuf.c:376`); `m_free()` clears it
  (`sys/kern/uipc_mbuf.c`).

## Impact

* **Reproduced: unprivileged local kernel panic** (DoS), two distinct
  signatures on the stock INVARIANTS kernel:
  * TCP path: `panic: assertion "m" failed in sorecvtcp at uipc_socket.c:1852`
  * AF_UNIX/UDP path: `panic: assertion "sb->sb_mb == m" failed in sbunlinkmbuf
    at uipc_sockbuf.c:552`
* The copier resumes copying out of **freed** mbuf clusters after waking
  (UAF read; `uiomove()` of freed memory) — on the INVARIANTS kernel the
  subsequent assert fires before userspace can harvest the bytes.
* On a **non-INVARIANTS (production) kernel** the AF_UNIX path has no assert:
  `sbunlinkmbuf()` then stores `sb->sb_mb = m->m_nextpkt` — a stale pointer
  read from freed memory — corrupting sockbuf state (potential deeper
  memory corruption), strictly worse than the panic.

## Files

* `poc_race.c`   — reproducer (mode 0 = TCP panic variant, mode 1 = AF_UNIX
  variant; both unprivileged). Pre-swaps its receive windows with a memory
  "pig" so every `recv()` page fault sleeps in `vm_fault`, which is what
  opens the token window (`lwkt_relalltokens`).
* `build.sh` / `run.sh` — exact build & run.
* `run.log`      — decisive baseline run output (mode 0 + mode 1).
* `panic.txt`    — TCP-path panic capture from the serial console.
* `panic2.txt`   — AF_UNIX-path panic capture from the serial console.
* `env.txt`      — guest uname / config.
* `fix.diff`     — the verified fix (sorflush waits for M_SOLOCKED mbufs).
* `verdict.json` / `manifest.json` / `VERDICT.md`.

## Build

    cc -O2 -pipe -pthread -o poc_race poc_race.c

## Run (as an unprivileged user)

    ./poc_race 40 3000 3000 0     # TCP variant -> panic in sorecvtcp:1852
    ./poc_race 60 200 500  1      # AF_UNIX variant -> panic in sbunlinkmbuf:552

Expected on a vulnerable kernel: guest becomes unreachable within ~2 minutes
(serial console shows one of the two panics above). Expected on a patched
kernel: `done: N rounds, 0 marker leaks` and the guest stays up.

## Notes

The marker scan ('P'-runs in the receive buffer) instruments the
info-leak variant; on INVARIANTS kernels the assert fires first, so the
leak is not harvestable there (see VERDICT.md).
