# DF-5537 — vkernel64 `vconswinch_intr()` missing tty token

## What (the defect)

`sys/platform/vkernel64/platform/console.c:272-289` — the SIGWINCH
interrupt handler `vconswinch_intr()` (registered INTR_MPSAFE at
console.c:374-377, so it runs concurrently with normal threads on other
CPUs) manipulates `ttyv0`'s token-protected state **without holding
`tp->t_token`**:

```c
if (vconsole != NULL && vconsole->cn_dev->si_tty != NULL) {        /* :276 */
        ioctl(0, TIOCGWINSZ, &newsize);
        /* ttioctl(...TIOCSWINSZ...); I wished.  Unfortunately this
           needs a curproc, so do it manually.  */                  /* :279 */
        if (bcmp(&newsize, &...->t_winsize, sizeof(newsize)) != 0) {/* :283 */
                ...->t_winsize = newsize;                          /* :285 */
                pgsignal(...->t_pgrp, SIGWINCH, 1);                /* :286 */
        }
}
```

Every other consumer of `t_winsize` / `t_pgrp` holds `tp->t_token`:
`vconsvirt_intr()` takes it (console.c:307), `ttioctl()`'s `TIOCSWINSZ`
(tty.c:1239-1243), `TIOCSPGRP`/`TIOCSCTTY` (tty.c:1205-1233),
`ttyclearsession()` (tty.c:283-292).  The commented-out line at
console.c:279-282 shows the author knew the manual version needed the
same serialization — and forgot it.  Additionally `vconsole` is loaded
four separate times (:276,:283,:285,:286) against `vconsterm()`'s
`vconsole = NULL` (:399) — a NULL-deref window during console teardown.

## What was verified on the guest (QEMU DFly 6.5-DEVELOPMENT)

Rig (proven DF-5337/DF-5289 family rig, upgraded): VKERNEL64
`kernel.debug` booted on a **pty controlling terminal** (`/vkroot.img`
root, `-n 2` vCPUs), so the driver can inject an arbitrarily fast
SIGWINCH storm by alternating `TIOCSWINSZ` rows 100↔101 on the pty
master.  Inside the vkernel, an **unprivileged (uid 9)** racer churns
ttyv0's foreground process group at full rate
(`setpgid(0,0)` → `TIOCSPGRP(&pg)` → park), forcing
`tp->t_pgrp` swaps + `pgrel()`→`kfree(M_PGRP)` ~12k/s.

Proven:
1. **The vulnerable path executes**: `stty size` on the console goes
   `0 0` → `101 80` across a storm (vconswinch_intr ran and wrote
   `t_winsize`); the churn child received **956,614 SIGWINCH in 45 s**
   — i.e. the token-less `pgsignal(tp->t_pgrp, …)` fired ~21k/s against
   the cycling pgrps (logs/vk5537-probe.log, logs/vk5537-full6.log).
2. **The free side runs**: 543,388/1,439,696 pgrp installs with the
   previous (empty) pgrp `pgrel`'d per swap — zero ioctl errors
   (logs/vk5537-full6.log, logs/vk5537-full5.log).
3. **No corruption manifested**: full-rate storm+churn for 45–90 s
   (≥10⁶ racy reads against ≥5×10⁵ frees), 3 independent runs — vkernel
   alive, racer completes cleanly.  Controls (churn-only, storm-only)
   also clean.

Why the UAF does not manifest in practice: `pgsignal()`'s **first**
action is `pgref(pgrp)` (kern_sig.c:917) — the token-less load of
`tp->t_pgrp` is pinned ~20 ns later, while the racing free path
(`tp->t_pgrp = new` store → `pgrel` → 1→0 → `LIST_REMOVE` → `kfree`)
takes microseconds; the dangerous interleaving (pin landing *after*
`kfree`) is arithmetically excluded in practice.  The remaining
exposure is the raw `t_winsize` data race and the `vconsole`/`vconsterm`
double-load — protocol defects, not demonstrated memory corruption.

## Verdict

`status: not_reproduced` (memory-safety impact), `impact: none` —
the lock-protocol violation itself is **proven present and exercised**
(items 1-2 above); the UAF it theoretically permits is self-pinned and
survived ~10⁶ forced opportunities without manifesting.  Severity Low
(hardening / correctness); fix recommended for protocol compliance.

## Fix

`fix.diff`: take `tp->t_token` around the winsize update + `pgsignal`,
cache `vconsole`/`cn_dev->si_tty` in locals.  Fix-validated on the
guest: patched vkernel survives the identical stress (689k installs,
1.86M storm ops, 1.39M SIGWINCH delivered, zero errors — see
logs/vk5537-fixed-full.log) and console winch propagation still works
(`stty size` `0 0` → `100 80`, logs/vk5537-fixed-probe.log).  No
baseline-bad/patched-good delta exists to demonstrate because the
corruption never manifested on the unpatched kernel either.

## Reproduce

On the audit guest (see `build.sh` / `run.sh`):

    # one-time: build vkernel + image + tools
    sh build.sh
    # decisive runs
    VKCON=/tmp/vkconsole.log /root/vkwinch_drv full 90     # race attempt
    VKCON=/tmp/vkconsole.log /root/vkwinch_drv churn 30    # control
    VKCON=/tmp/vkconsole.log /root/vkwinch_drv plain 20    # control + stty probe

Success criterion for the *violation*: `stty size` changes across the
storm and `[race child] … sigwinch=<large>` in the console log.
Success criterion for the *corruption* (never observed): Fatal
trap/panic/pgrel assertion in the vkernel under `full` mode.
