# DF-2897 — mute/close state-machine leak in /dev/console bookkeeping

## Summary (analysis-only; Low severity, deterministic, root-gated)

`cnclose()` (sys/kern/tty_cons.c:403-404) returns immediately — without
updating `cn_is_open` / `cn_phys_is_open` — whenever the console is muted
(`cn_fwd_ops == NULL`, set by `cnuninit()` at :248). The un-mute path
`sysctl_kern_consmute()` (tty_cons.c:265-278) then re-opens the physical
console device purely on the strength of the stale `cn_is_open` flag with
the stale `openflag`/`openmode` snapshot from the previous opener.

Sequence (no race required):

1. Root opens /dev/console (cnopen sets `cn_is_open=1`, snapshots
   `openmode/openflag`, forwards open to the physical console dev).
2. Root mutes: `sysctl kern.consmute=1` → dev_dclose(cn_dev) forwarded,
   `cnuninit()` restores ops, `cn_fwd_ops = NULL`, `cn_dev = NULL`.
   `/dev/console` fd is still open, `cn_is_open` stays 1 — correct so far.
3. Root closes the /dev/console fd → cnclose hits
   `cn_tab == NULL || cn_fwd_ops == NULL` at :403 → **returns 0 without
   clearing cn_is_open (or cn_phys_is_open)**.
4. Root un-mutes: `sysctl kern.consmute=0` → cninit_finish() re-hooks,
   then `if (cn_is_open) dev_dopen(cn_dev, openflag, openmode, ...)`
   re-opens the physical console device **although nobody holds
   /dev/console anymore**.

Effects: the underlying console device (e.g. the serial/ttyv tty) is held
open forever with no file descriptor behind it — it can no longer be
closed through /dev/console (subsequent /dev/console opens/closes
desync against the phantom open; DF-0124's unsynchronized bookkeeping
amplifies this), and it is re-opened with a stale flags snapshot from a
previous, possibly different, privileged opener.

Same stale flag applies to `cn_phys_is_open` when the *physical* device
is closed while muted (the intercept is removed, so the close bypasses
cnclose entirely): a later /dev/console close then declines to forward
the close (`if (cn_phys_is_open) return(0)` at :429-430) and the real
device close is never delivered.

Preconditions: root must toggle `kern.consmute` around the close — an
administrative action (the sysctl is privileged), hence Low severity:
state-machine/resource bug, not an unprivileged attack surface.

## Why no Phase V

Low severity, root-gated, no memory-safety consequence — per the audit
contract Phase V is reserved for Critical/High or memcorrupt/privesc
findings. The logic above is fully deterministic and citable to source;
`verdict.json` records status `untested` for the DB.

## Suggested fix (sketch, not applied)

In `cnclose()`, update the bookkeeping before the muted early-return:

```c
if (cn_tab == NULL || cn_fwd_ops == NULL) {
        /* keep bookkeeping in sync even while muted */
        if (dev == cn_devfsdev || dev == cn_tab->cn_dev) {
                if (dev == cn_tab->cn_dev)
                        cn_phys_is_open = 0;
                else
                        cn_is_open = 0;
        }
        return (0);
}
```

(and have the un-mute path re-open only when `cn_is_open` legitimately
reflects a live /dev/console fd).
