β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2897

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2897 Β· 3 files
FileTypeDescriptionSize
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):

  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:

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_testable
per-fix-DF-2897

Confirmed 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.