# DF-2896 VERDICT — cnwrite() constty use-after-free race: NOT REPRODUCED (window code-proven)

**Status: not_reproduced. reproduced=0, observed impact=none.
Structural confidence in the race window: certain (code-proven, cited).
Manifestation on this guest within budget: not achieved.**

## The bug, proven from source

1. **Unprivileged lifetime control (default config).**
   `sys/config/X86_64_GENERIC:36` — `options UCONSOLE`. With UCONSOLE the
   privilege check in the TIOCCONS handler is compiled out
   (`sys/kern/tty.c:969-977`): any user's pty can become `constty`.
   Verified live on the guest: `gate_test.c` run as uid=1001 returned
   "TIOCCONS OK".

2. **Capture without synchronization.**
   `sys/kern/tty_cons.c:465-466`:
   ```c
   if (constty)
           dev = constty->t_dev;
   ```
   cnwrite runs MPSAFE (cn_ops D_MPSAFE, tty_cons.c:71) on the writer's
   thread with no token; `constty` and `constty->t_dev` are read racily.
   Compare the mutation sites, all of which run under `tp->t_token`
   (tty.c:251-252 ttyclose; tty_pty.c:~405 ptsclose holds
   `pti->pt_tty.t_token` across ttyclose+pti_done) — or with no token at
   all (tty_cons.c:493 cnioctl; subr_prf.c:395 kputchar-on-panic).

3. **Sleepable gap between capture and use.**
   `tty_cons.c:469-471`: `log_console(uio)` runs *between* the capture and
   `dev_doperate(&ap->a_head)`. log_console (subr_prf.c:259-299) does
   `kmalloc(M_WAITOK)` (sleeps), copies the iovec array, then walks the
   **entire** write buffer 128 bytes at a time with one `msglogchar()`
   per byte. For a 4MB console write this is a ~100ms+ window; it scales
   without bound with write size.

4. **The captured object dies inside that window, attacker-driven.**
   Closing the pty pair: ptsclose -> ttyclose -> `constty = NULL`
   (tty.c:252); then (both sides closed) pti_done ->
   `t_dev = NULL; destroy_dev(devs); destroy_dev(devc)`
   (tty_pty.c:280-291). destroy_dev -> devfs_destroy_dev_worker ->
   release_dev x3 -> sysref terminate -> the cdev memory returns to the
   dedicated cdev objcache (devfs_core.c:63-75, sysref class "cdev").

5. **Use-after-free dispatch.**
   `dev_doperate()` (kern/kern_device.c:544-561) reads
   `ap->a_dev->si_ops` from the (possibly freed) cdev and indirect-calls
   through it. On slot recycle, `devfs_new_cdev()` **bzeroes the struct
   up to si_sysref** (devfs_core.c:2445) — si_ops is inside that range —
   and sets `si_ops = ops` only afterwards, under the contended devfs
   lock. A dispatch landing in that transient reads `si_ops == NULL` ->
   guaranteed kernel page fault (panic). A dispatch landing after
   re-init writes root's console data into an unrelated pty (misdirected
   write); before re-init it dispatches through stale static pty ops into
   a dying tty (EIO).

## Verification attempts (8 harness designs, 2 genuinely-racing configurations)

| run | config | genuine racing? | result |
|-----|--------|-----------------|--------|
| 1   | 6x256KB writers, spray cycler | ~90s (pre-wedge) | console wedged by firehose, no panic text |
| 2-3 | binaries under /root | **no** — unpriv actor got EACCES (harness bug, found later) | invalid |
| 4   | 2x32MB + holder | racer stuck (0 iters; holder perm denied) | invalid; guest hard-down, no text |
| 5   | 1x4MB + holder | racer status unknown (ssh starved) | guest hard-down, no text |
| 6   | serial-quiet cycler, 1x4MB | **yes** — 2368 cycles, 704 dispatches | no panic, holder absent (not rebuilt) |
| 7   | same + holder **present** | **yes** — 4864 cycles, holder live, ~1300 dispatches | no panic, 0 HITs |

Total genuine exposure: ~7000 teardowns, ~2000 capture->dispatch
envelopes each 120ms wide, with active recycling pressure and a
deterministic misdirection detector ('W' bytes on never-TIOCCONS'd
ptys). **No manifestation.**

## Why it did not manifest (analysis)

- The cdev teardown is **asynchronous through the devfs core thread**
  (destroy_dev -> devfs_msg_send_dev). Under pty churn its latency
  stretches well past a single write's envelope, so most dispatches land
  while the cdev is dying-but-allocated (stale static ops -> EIO, benign).
- The cdev objcache is **dedicated to sizeof(struct cdev)** with per-cpu
  magazines. Frees happen on the devfs thread's cpu; the unprivileged
  allocator pressure runs on other cpus; slot migration goes through the
  depot in batches. The recycle (and its ~microsecond si_ops==NULL
  transient) is therefore only weakly correlated with any given
  dispatch instant.
- The two observed hard-downs (runs 4-5) left **no panic text** in the
  serial log (105MB inspected) and cannot be attributed to this bug
  rather than to console firehose starvation in general; per the honest
  classification rules they do not count as reproduction.

## Impact ceiling (if manifested)

- Panic (NULL si_ops) — local DoS contributed by an unprivileged user,
  requires a concurrent privileged /dev/console writer (default syslogd
  logs *.err;kern.warning to /dev/console).
- Misdirected kernel write of privileged console output into an
  unrelated user pty.
- Full RIP control is NOT credible: the recycled slot can only be
  another cdev (dedicated objcache, kernel-initialized contents) and the
  stale ops pointers are static kernel structures. No attacker-controlled
  function pointer materializes. uid=0 escalation: not plausible from
  this primitive alone.

## Exploit chain

none demonstrated; see above for the theoretical ceiling.

## Fix

`fix.diff` (this pack): capture `constty->t_dev` under `tp->t_token`
(the same token the teardown path holds) and hold the token across the
forward. Fix validation: **not_testable** — no baseline reproduction
exists to diff against.

## kernel_refs

- sys/kern/tty_cons.c:456-472 (cnwrite capture/use, unsynchronized)
- sys/kern/tty_cons.c:71 (D_MPSAFE)
- sys/kern/tty.c:959-977 (UCONSOLE TIOCCONS, no priv check)
- sys/kern/tty.c:251-252 (ttyclose clears constty under t_token)
- sys/kern/tty_pty.c:280-291 (pti_done: t_dev=NULL, destroy_dev)
- sys/kern/subr_prf.c:259-299 (log_console sleeps/walks whole buffer)
- sys/kern/kern_device.c:544-561 (dev_doperate reads si_ops, indirect call)
- sys/vfs/devfs/devfs_core.c:63-75 (dedicated cdev sysref/objcache)
- sys/vfs/devfs/devfs_core.c:2436-2459 (devfs_new_cdev bzero incl. si_ops)
- sys/config/X86_64_GENERIC:36 (options UCONSOLE, default kernel)
