Mute-interaction bookkeeping leak: cnclose()'s muted early-return never clears cn_is_open/cn_phys_is_open, so un-muting re-opens the physical console device with a stale flags snapshot for a /dev/console that is no longer open
| Field | Value |
|---|---|
| ID | DF-2897 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-401 / CWE-445 |
| File | sys/kern/tty_cons.c |
| Lines | 403-404, 415, 429-430, 265-278 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
Deterministic, no race: open /dev/console (cn_is_open=1, flags
snapshotted) β mute (forwards a close, NULLs cn_fwd_ops/cn_dev) β
close the fd β cnclose() hits the muted early-return and returns 0
WITHOUT clearing cn_is_open β un-mute runs if (cn_is_open)
dev_dopen(cn_dev, openflag, openmode, ...) and re-opens the physical
console device although no descriptor holds /dev/console anymore β
wedged open forever with a stale flags snapshot from a possibly
different privileged opener. Complements DF-0124 (concurrent desync);
this is the sequential mute-interaction hole. Root-gated, no memory
safety impact. Reproduce by hand: exec 3<>/dev/console; sysctl
kern.consmute=1; exec 3>&-; sysctl kern.consmute=0. Fix: update
bookkeeping before the muted early-return (row diff).
Timeline
- 2026-09-02 Discovered during pass-2 audit of tty_cons.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2897 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.1 KB | β raw | |
| verdict.json | β | 2.0 KB | view raw | |
| manifest.json | β | 803 B | view raw |
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):
- Root opens /dev/console (cnopen sets
cn_is_open=1, snapshotsopenmode/openflag, forwards open to the physical console dev). - Root mutes:
sysctl kern.consmute=1β dev_dclose(cn_dev) forwarded,cnuninit()restores ops,cn_fwd_ops = NULL,cn_dev = NULL./dev/consolefd is still open,cn_is_openstays 1 β correct so far. - Root closes the /dev/console fd β cnclose hits
cn_tab == NULL || cn_fwd_ops == NULLat :403 β returns 0 without clearing cn_is_open (or cn_phys_is_open). - Root un-mutes:
sysctl kern.consmute=0β cninit_finish() re-hooks, thenif (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:
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).
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-2897/README.md β full four-step deterministic walkthrough with path:line citations and fix sketch']
PoC changes
not applicable (analysis-only)
Verified recommended fix
Update cn_is_open/cn_phys_is_open before cnclose()'s muted early-return so the un-mute path cannot re-open the physical console device with stale flags for a /dev/console that is no longer open.
Verdict
Deterministic state-machine defect, verified by source walkthrough only (no runtime test; Low severity, root-gated, no memory-safety impact β Phase V not warranted per contract). cnclose()'s muted early-return (sys/kern/tty_cons.c:403-404) skips all bookkeeping, so cn_is_open/cn_phys_is_open survive a /dev/console (or physical-device) close that happens while kern.consmute=1; the subsequent un-mute in sysctl_kern_consmute (tty_cons.c:265-278) then unconditionally re-opens the physical console device (dev_dopen(cn_dev, openflag, openmode,...)) with a stale flags snapshot although no descriptor holds /dev/console, leaving the underlying console device permanently open with no way to close it via /dev/console. Every step is unconditional straight-line code β no race needed.
No comments yet.