# DF-2896 — cnwrite() constty UAF race — evidence pack

## What this is

`sys/kern/tty_cons.c:cnwrite()` (lines 456-472) captures `constty->t_dev`
(a raw `cdev_t`, no reference, no token) and then sleeps/walks the entire
write buffer in `log_console()` (kern/subr_prf.c:259, M_WAITOK kmalloc +
uiomove + one msglogchar per byte) before dispatching `dev_doperate()` on
the captured cdev. An unprivileged user can control the lifetime of that
cdev on default kernels: `options UCONSOLE` (sys/config/X86_64_GENERIC:36)
lets any user point `constty` at their own pty via TIOCCONS
(sys/kern/tty.c:959-977), and closing the pty pair runs
`ttyclose()` -> `constty = NULL` (tty.c:251-252), then `pti_done()` ->
`t_dev = NULL`, `destroy_dev(devs/devc)` (kern/tty_pty.c:280-291), which
frees the cdev on the devfs thread. cnwrite holds no lock against any of
this — the window between capture and dispatch is the whole log_console()
walk (milliseconds to seconds, scaling with write size).

Consequences at dispatch through a freed cdev (kern/kern_device.c:544
dev_doperate reads `ap->a_dev->si_ops` and indirect-calls):
- slot freed, not yet recycled: stale `si_ops` points at static pty ops ->
  ptswrite on a dying tty -> EIO (benign).
- slot recycled mid-init: `devfs_new_cdev()` bzeroes si_ops
  (devfs_core.c:2445) before setting it -> `si_ops == NULL` ->
  NULL-pointer kernel fault -> panic.
- slot recycled into another pty: root's console bytes are written into an
  unrelated user pty (misdirected kernel write / information leak).

## Gate test (passed)

`gate_test.c`: unprivileged uid=1001 on the stock guest kernel
(X86_64_GENERIC, UCONSOLE) successfully sets constty via TIOCCONS on its
own pty — the unprivileged lifetime-control side of the race is confirmed:

    $ id; /tmp/gate_test
    uid=1001(maxx) gid=1001(groups=1001)
    GATE: TIOCCONS OK for uid=1001 (constty now our pty)

## Build

On the guest (as root):

    cc -O2 -o racer racer.c
    cc -O2 -pthread -o writer writer.c
    cc -O2 -o holder holder.c
    cc -O2 -o pin pin.c

## Run

    /tmp/df2896/run.sh     # starts: racer (unpriv), holder (unpriv),
                           # writer (root, 1 thread x 4MB /dev/console)
    # poll: grep -c HIT /tmp/df2896/holder.log   (misdirection detector)
    #       grep -a "Fatal trap\|panic:" <serial boot.log>

Actors:
- `racer` (uid 1001): cycles constty — open ptmx/pts, TIOCCONS, hold
  12ms (draining the master), close both (teardown), immediately re-attach
  constty on the next pty.
- `holder` (uid 1001): opens 192-pty generations, watches every master for
  the writer fill byte 'W'. These ptys are NEVER TIOCCONS'd, so any 'W'
  byte proves a dispatch through a recycled stale cdev (misdirection).
- `writer` (root): 1 thread, 4MB writes to /dev/console, 3/s — each write
  captures constty->t_dev, sleeps ~120ms in log_console, then dispatches.

## Expected result (success criterion)

Either
- kernel panic ("Fatal trap" / NULL deref in dev_doperate from cnwrite), or
- `HIT: N W-bytes on /dev/pts/X (never TIOCCONS'd)` in holder.log.

## Observed result

NOT REPRODUCED within ~20 minutes of genuine racing
(~7000 constty teardowns, ~2000 capture->dispatch envelopes, holder
active). No panic, zero misdirection hits. Two earlier hard-downs under
heavier load were unattributable (no backtrace; console firehose).

See VERDICT.md for why the window is real but did not manifest on this
guest (async devfs teardown latency + dedicated-cdev objcache magazine
batching decouple the free/realloc from the dispatch instants).
