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

Unprivileged kernel-log/kernel-pointer disclosure channel: default-config UCONSOLE lets any user's pty become constty and subr_prf.c streams all kernel messages to it, bypassing security.unprivileged_read_msgbuf and nullifying ptr_restrict against the %p leak population

Field Value
ID DF-2759
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
CWE CWE-200 / CWE-552
File sys/kern/subr_prf.c
Lines 396-405, 913-985, 1134-1143 (gate tty.c:969-976, config :36)
Area kern
Confidence certain
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket kernleak
Reported pending
Known CVE none
CVE match novel

Summary

The stock amd64 kernel config ships options UCONSOLE, which compiles the caps_priv_check gate out of the TIOCCONS handler. Any unprivileged local user therefore installs their own pty as constty. subr_prf.c then treats that pty as the trusted console intercept: kputchar diverts all console-destined output to TOLOG|TOWAKEUP whenever constty is set, constty_daemon streams the msgbuf to constty's tty forever, and cnwrite additionally redirects /dev/console writes to constty->t_dev. Verified live on the stock guest: uid-1001 TIOCCONS succeeds; root console markers reach the unprivileged pty instantly (cnwrite redirect) and again ≀1s later as <118>-tagged constty_daemon msgbuf copies; a pure kprintf is delivered; and delivery continues after root sets security.unprivileged_read_msgbuf=0 while the same user's dmesg gets EPERM β€” that knob only gates the sysctl path, not the constty channel. With ptr_restrict=0 default this is a silent, persistent, real-time collection channel for every %p kernel-pointer print in the tree (amplifies DF-2736, DF-2614, DF-2637, and any future %p print).

Threat model & preconditions

Unprivileged local user obtains a live kernel-message feed on their own terminal: kernel/heap pointers (KASLR and slab-layout defeat, feeding any companion memory-corruption bug), kernel operational logs, and all future kprintf content — surviving admin attempts to lock dmesg away. Persistent and undetectable by the console operator (output is duplicated, not diverted). No user→root route (info-disclosure class).

Proof of contest

VERIFIED on the stock guest (findings/poc/DF-2759/uconsole_grab.c): unpriv TIOCCONS SUCCESS; instant + delayed msgbuf-tagged marker delivery; delivery continues after unprivileged_read_msgbuf=0; pure kprintf delivered. Fix (drop UCONSOLE from X86_64_GENERIC β€” the resolution FreeBSD adopted for GENERIC in 2003 β€” and make the TIOCCONS privilege check unconditional in tty.c) validated in-guest: PoC now gets EPERM with zero kernel content on the pty, root TIOCCONS still works.

Both hunks in findings/poc/DF-2759/fix.diff (either alone closes the default-config hole; both shipped).

Timeline

  • 2026-08-30 Discovered during pass-2 audit of subr_prf.c (GLM 5.3); unpriv live-feed reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2759 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 5.2 KB ↓ raw
uconsole_grab.c β€” 2.8 KB view raw
build.sh β€” 173 B view raw
run.sh β€” 1.7 KB view raw
build.log β€” 68 B view raw
run.log β€” 1.2 KB view raw
run_patched.log β€” 132 B view raw
env.txt β€” 265 B view raw
fix.diff β€” 923 B view raw
fix_build.log β€” 951 B view raw
verdict.json β€” 4.9 KB view raw

DF-2759 β€” Unprivileged kernel-log/kernel-pointer disclosure via UCONSOLE TIOCCONS + constty_daemon

What

The stock DragonFly amd64 kernel config sys/config/X86_64_GENERIC (ident the guest actually runs: X86_64_GENERIC) is built with options UCONSOLE (line 36). That option compiles the caps_priv_check_td() gate out of the TIOCCONS handler (sys/kern/tty.c:969-976), so any unprivileged user can install their own pty as the kernel console intercept:

  • sys/kern/tty.c:977 β€” constty = tp; with no privilege check.
  • sys/kern/subr_prf.c:396-397 (kputchar) β€” once constty is set, all TOCONS output is also copied to the msgbuf and the constty daemon is woken.
  • sys/kern/subr_prf.c:913-985 (constty_daemon) β€” streams msgbuf content to constty's tty, forever, with no additional authorization.
  • sys/kern/tty_cons.c:465 (cnwrite) β€” every write(2) to /dev/console is redirected to constty->t_dev (the attacker's pty).

Impact

An unprivileged local user receives a real-time stream of all kernel messages on their own terminal:

  • bypasses security.unprivileged_read_msgbuf=0 (that knob only gates the sysctl read path, subr_prf.c:1134-1143);
  • turns every %p kernel-pointer print (default security.ptr_restrict=0, subr_prf.c:121) into an unprivileged, live pointer leak β€” amplifying the filed consumer-side leaks (DF-2736, DF-2614, DF-2637, ...);
  • persistent and silent: the operator's console still shows everything, the attacker just gets a duplicate feed.

Reproduce (host, guest already up)

scp -F dfbsd-qemu/config uconsole_grab.c dfbsd-maxx:/tmp/
vm.sh run_root 'mkdir -p /tmp/df2759 && chmod 777 /tmp/df2759 && mv /tmp/uconsole_grab.c /tmp/df2759/ && chown maxx:maxx /tmp/df2759/uconsole_grab.c'
vm.sh run_user 'cd /tmp/df2759 && cc -O -Wall -o uconsole_grab uconsole_grab.c'   # build.sh
sh findings/poc/DF-2759/run.sh                                                    # run.sh

Expected (baseline, stock kernel)

[0.00] uid=1001 ... TIOCCONS: SUCCESS - constty now points at this UNPRIVILEGED pty
[3.02] master received 29 bytes:  DF2759-MARKER1-direct+daemon            <- direct cnwrite redirect
[3.56] master received 35 bytes:  <118>DF2759-MARKER1-direct+daemon       <- constty_daemon msgbuf stream
       dmesg: sysctl kern.msgbuf: Operation not permitted                   <- user locked out of msgbuf
[8.56] master received 79 bytes:  <118>DF2759-MARKER2-after-msgbuf-locked
                                  tap0: MAC address: 00:bd:ab:30:01:00     <- pure kprintf delivered too

The <118>-tagged copies arriving AFTER security.unprivileged_read_msgbuf was set to 0 prove both the msgbuf-stream channel and the knob bypass.

Fix validation

fix.diff removes options UCONSOLE from X86_64_GENERIC and makes the TIOCCONS privilege check unconditional in sys/kern/tty.c. With the patched kernel the same PoC prints:

TIOCCONS FAILED: Operation not permitted (UCONSOLE gate not present in this kernel)

and no kernel content reaches the pty. See VERDICT.md.

Contents

  • uconsole_grab.c β€” PoC (unprivileged pty grab + master-side capture)
  • build.sh / run.sh β€” exact commands
  • build.log / run.log β€” untrimmed logs
  • env.txt β€” guest identity, knobs
  • fix.diff β€” git-apply-able fix (validated in-guest)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-2759 β€” VERDICT

Status: REPRODUCED (impact: leak β€” unprivileged kernel-log/kernel-pointer disclosure channel)

What was claimed

options UCONSOLE in the default sys/config/X86_64_GENERIC:36 removes the privilege check from TIOCCONS (sys/kern/tty.c:969-976), so any unprivileged user can point the kernel's constty at their own pty. subr_prf.c then treats that pty as the console intercept:

  • sys/kern/subr_prf.c:396-405 β€” kputchar(): with constty set, all console-destined kprintf output is additionally logged to the msgbuf and wakes the constty daemon (TOWAKEUP).
  • sys/kern/subr_prf.c:913-985 β€” constty_daemon(): streams the msgbuf (all subsequent kernel messages) to constty's tty, no authorization.
  • sys/kern/tty_cons.c:465 β€” cnwrite(): user writes to /dev/console are redirected to constty->t_dev.

Bypass relationship: security.unprivileged_read_msgbuf gates only the sysctl read path (sys/kern/subr_prf.c:1134-1143); it does not touch the constty channel. security.ptr_restrict defaults to 0 (sys/kern/subr_prf.c:121), so %p prints (the DF-2736/DF-2614/DF-2637 consumer-side leak population) flow through this channel unmasked.

How it was verified (stock kernel #0, X86_64_GENERIC, guest "dfbsd")

  1. Built uconsole_grab.c as user maxx (uid 1001) in-guest (build.log).
  2. ioctl(slave, TIOCCONS, 1) as uid 1001 β†’ SUCCESS (run.log line [0.00] TIOCCONS: SUCCESS).
  3. Root echo DF2759-MARKER1 > /dev/console: * [3.02] instant copy on the attacker's master (cnwrite redirect), and * [3.56] <118>DF2759-MARKER1... β€” the constty_daemon msgbuf stream copy (syslog pri tag <> is added by msglogchar, subr_prf.c:1014-1023 β€” unambiguous marker of the daemon/msgbuf path).
  4. Root sysctl -w security.unprivileged_read_msgbuf=0; user dmesg β†’ sysctl kern.msgbuf: Operation not permitted; root echo DF2759-MARKER2 > /dev/console β†’ [8.56] <118>MARKER2 still delivered to the unprivileged pty β†’ the security knob is bypassed.
  5. ifconfig tap0 create (root; a pure kprintf, no console write) β†’ tap0: MAC address: 00:bd:ab:30:01:00 delivered to the unprivileged pty β†’ the channel carries arbitrary kernel log content, i.e. every %p print would equally be delivered (ptr_restrict=0 confirmed live in env.txt).

Why it is not higher than Medium

The channel discloses kernel log content (including kernel/heap pointers via existing %p prints) to unprivileged users on the default config, defeating two admin-facing mitigations. It is not itself an arbitrary-read primitive. It is the amplifier that makes the filed %p consumer leaks (rated Low individually) trivially collectible by any local user.

Fix validation (fix.diff β€” applied to guest /usr/src, kernel rebuilt)

  • fix.diff (git-apply-verified on the host repo) removes options UCONSOLE from sys/config/X86_64_GENERIC AND makes the TIOCCONS caps_priv_check_td(SYSCAP_RESTRICTEDROOT) unconditional in sys/kern/tty.c (either hunk alone closes the default-config hole).
  • make -j6 nativekernel && make installkernel in-guest (fix_build via /tmp/fixbuild.log), rebooted into DragonFly 6.5-DEVELOPMENT #1: Mon Aug 31 21:24:15 UTC 2026.
  • Re-ran the identical PoC as uid 1001 (run_patched.log): [0.00] TIOCCONS FAILED: Operation not permitted β€” no kernel content reached the pty (marker writes and the tap0 kprintf produced zero bytes on the master).
  • Root cross-check on the patched kernel: TIOCCONS: SUCCESS for uid 0 β€” the gate restores intended semantics, not breakage.

False-positive considerations β€” killed candidates from this pass

(pass-2 depth; recorded so the negative result has weight) * constty dangling-pointer UAF (constty_daemon unlocked tp = constty): ptys never free pti (sys/kern/tty_pty.c:279 "pti structure remains intact"), nmdm never frees its softc (no kfree in sys/dev/misc/nmdm/), serial softcs persist; a stale constty always lands in live memory and tputchar returns -1 once TS_CONNECTED is cleared (ttyclose, sys/kern/tty.c:251-262). * uprintf/tprintf torn s_ttyvp/s_ttyp read β†’ NULL tputchar: s_ttyp is stored before s_ttyvp (tty.c:1197 β†’ devfs_vnops.c:1606), and both old/new ttys are ttyhold()-referenced (tty.c:1189-1199); s_ttyvp != NULL implies a valid s_ttyp. * %px ptr_restrict escape: zero in-tree users. * kprintf_logging/log_console_output RW sysctls: writes are caps-gated (sys/kern/kern_sysctl.c:1446-1447). * %pb%i divide-by-zero on base char \0 / 1 << (n-1) UB: all 163 in-tree bit-strings are compile-time constants (rg-verified). * width-accumulation int overflow in the digits loop (subr_prf.c:652-657): kernel-constant formats only; PCHAR output is bounded by the sink. * sysctl_kern_msgbuf third-branch length n - rindex_modulo: KNOWN (DF-0035 / DF-2586 family) β€” not re-reported; msgbuf_clear races and msgaddchar MP races stay within the tolerant chunk-advance protocol (subr_prf.c:1060-1071). * msg_size < 2048 breaking xindex - msg_size + 2048: unreachable (MSGBUF_SIZE = 1 MB, sys/sys/msgbuf.h:65). * kvsnprintf truncation/NUL logic (subr_prf.c:456-467, 493-502): correct.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (config UCONSOLE removal + unconditional caps gate in tty.c) applied to guest /usr/src, nativekernel rebuilt and installed, guest rebooted into kernel #1. Identical PoC as uid 1001: TIOCCONS now fails with EPERM and no kernel content reaches the pty (run_patched.log); root TIOCCONS still succeeds β€” gate restored, feature intact for privileged use.

['fix.diff (git apply --check clean on the audit repo)', "fix_build.log: '>>> Kernel install for X86_64_GENERIC completed' + patched-kernel uname #1", "run_patched.log: 'TIOCCONS FAILED: Operation not permitted' / zero bytes captured", 'root cross-check: uid=0 TIOCCONS SUCCESS on patched kernel']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Aug 31 21:24:15 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

unpriv user: posix_openpt -> ioctl(slave, TIOCCONS, 1) [no privilege needed under UCONSOLE] -> read(master): real-time full kernel-log stream incl. every %p pointer print; persists silently after the user locks dmesg via security.unprivileged_read_msgbuf=0. Pointer values feed KASLR/heap-layout disclosure for any companion memory-corruption bug.

Evidence (decisive lines)

["run.log: '[0.00] TIOCCONS: SUCCESS - constty now points at this UNPRIVILEGED pty' (uid=1001)", "run.log: [3.56] '<118>DF2759-MARKER1-direct+daemon' β€” constty_daemon msgbuf copy (pri tag from msglogchar subr_prf.c:1014-1023)", "run.log: dmesg EPERM + [8.56] '<118>DF2759-MARKER2-after-msgbuf-locked' β€” security.unprivileged_read_msgbuf bypass", "run.log: 'tap0: MAC address: ...' β€” pure kprintf content delivered to the unpriv pty", "run_patched.log: 'TIOCCONS FAILED: Operation not permitted' on the fix.diff kernel; root cross-check still succeeds", 'env.txt: ptr_restrict=0 default confirmed live; uname shows X86_64_GENERIC']

PoC changes

Seed PoC was written fresh for this finding (no prior draft): posix_openpt/grantpt/unlockpt pty setup, raw termios on the slave to avoid echo/CR mangling, TIOCCONS with int 1, poll+read loop on the master with monotonic timestamps so the direct-forward vs delayed daemon copies are distinguishable.

Verified recommended fix

Remove 'options UCONSOLE' from sys/config/X86_64_GENERIC and make the TIOCCONS caps_priv_check_td(SYSCAP_RESTRICTEDROOT) unconditional in sys/kern/tty.c (fix.diff, validated in-guest).

Verdict

Reproduced on the stock guest kernel (X86_64_GENERIC, which ships options UCONSOLE at sys/config/X86_64_GENERIC:36). Unprivileged uid-1001 ioctl(TIOCCONS) succeeds because the caps_priv_check gate is compiled out (sys/kern/tty.c:969-976); constty then points at the attacker's pty and subr_prf.c's kputchar (subr_prf.c:396-405) + constty_daemon (subr_prf.c:913-985) stream all kernel messages to it, while cnwrite (tty_cons.c:465) redirects /dev/console writes there too. Demonstrated live: root console markers arriving on the unprivileged pty both instantly and as <118>-tagged constty_daemon msgbuf copies; a pure kprintf (tap0 attach) delivered; and delivery continuing after security.unprivileged_read_msgbuf=0 while the same user's dmesg gets EPERM β€” a direct bypass of the admin knob, and with ptr_restrict=0 default it is a live collection channel for the entire %p kernel-pointer leak population (DF-2736/2614/2637 consumer side).