โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0006

kern.ttys sysctl leaks kernel function/heap pointers to unprivileged users

Field Value
ID DF-0006
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
CWE CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
File sys/kern/tty.c
Lines 2891-2924
Area kern
Confidence certain
Discovered 2026-06-29
Reported pending

Summary

The kern.ttys sysctl handler (sysctl_kern_ttys) copies each struct tty verbatim to userspace. struct tty embeds kernel function pointers (t_oproc, t_stop, t_param, t_unhold), kernel heap object pointers (t_pgrp, t_session, t_sigio, t_sc, t_slsc), per-clist data buffer pointers (t_rawq/t_canq/t_outq .c_data), and an embedded lwkt_token. Only t_dev is sanitized. Because sysctl reads are not privilege-gated in DragonFlyBSD (sysctl_root checks privilege only for writes), any unprivileged local user can dump all of these addresses, defeating KASLR and revealing kernel heap layout.

Root cause

sysctl_kern_ttys (sys/kern/tty.c:2891-2921) builds a local copy t = *tp; of the entire struct tty and emits it with SYSCTL_OUT(req, (caddr_t)&t, sizeof(t)). The only field rewritten before copyout is t_dev, via devid_from_dev (tty.c:2912-2913). Every pointer field in sys/sys/tty.h is copied raw: t_pgrp, t_session, t_sigio, t_oproc/t_stop/t_param/t_unhold, t_sc/t_slsc, and the short *c_data in each of t_rawq/t_canq/t_outq, plus the embedded t_token. The OID is registered SYSCTL_PROC(_kern, OID_AUTO, ttys, CTLTYPE_OPAQUE|CTLFLAG_RD, ...) (tty.c:2923-2924); sysctl_root (kern_sysctl.c) applies its privilege/securelevel checks only when req->newptr is set (writes), so a plain read sets no newptr and any user may read kern.ttys. (Compare kern_descrip.c, which exports file data through a purpose-built, sanitized kinfo_file struct rather than the raw struct filedesc โ€” tty.c does not follow that pattern.)

Threat model & preconditions

  • Attacker position: any local unprivileged user.
  • Privileges gained or impact: information disclosure. Exposes (a) kernel .text addresses via the function pointers (for ptys t_oproc/t_stop/ t_unhold point to ptsstart/ptsstop/ptsunhold in tty_pty.c) โ€” a precise KASLR-relocation primitive; (b) kernel heap addresses (pgrp, session, sigio, per-queue c_data buffers) usable to refine heap grooming for a separate heap-corruption bug; (c) token internals. No arbitrary memory contents are disclosed.
  • Required config or capabilities: none; default kernel.
  • Reachability: sysctlbyname("kern.ttys", ...) as any user.

Proof of concept

PoC source: findings/poc/DF-0006/leak_ttys.c

Reads the kern.ttys blob and scans it for pointer-sized values that look like kernel addresses, proving the leak without depending on the exact arch-dependent struct tty layout.

Build & run

cc -o leak_ttys findings/poc/DF-0006/leak_ttys.c
./leak_ttys        # as a non-root user

Expected output

got 4320 bytes from kern.ttys
  blob offset    16 (word     2): 0xffff8000xxxxxxxx
  ...
total kernel-range pointer-sized values leaked: N

Non-zero N confirms the leak.

Impact

Lowers the bar for exploiting any future local kernel memory-corruption bug in the tty or adjacent subsystems by defeating KASLR and revealing heap layout. Information disclosure only (addresses, not arbitrary memory); rated Low.

Sanitize every pointer field of the local copy before SYSCTL_OUT, or better, export a dedicated, pointer-free kinfo_tty structure (mirroring the kinfo_file pattern in kern_descrip.c). Minimal diff that zeros all kernel pointers while preserving the fields pstat(8) actually uses:

--- a/sys/kern/tty.c
+++ b/sys/kern/tty.c
@@ -2911,6 +2911,18 @@ sysctl_kern_ttys(SYSCTL_HANDLER_ARGS)
        t = *tp;
        if (t.t_dev)
            t.t_dev = (cdev_t)(uintptr_t)devid_from_dev(t.t_dev);
+       /* Do not leak kernel pointers to userspace. */
+       bzero(&t.t_token, sizeof(t.t_token));
+       t.t_pgrp = NULL;
+       t.t_session = NULL;
+       t.t_sigio = NULL;
+       t.t_rawq.c_data = NULL;
+       t.t_canq.c_data = NULL;
+       t.t_outq.c_data = NULL;
+       t.t_oproc = NULL;
+       t.t_stop = NULL;
+       t.t_param = NULL;
+       t.t_unhold = NULL;
+       t.t_sc = NULL;
+       t.t_slsc = NULL;
        error = SYSCTL_OUT(req, (caddr_t)&t, sizeof(t));
        if (error)
            break;

A stronger long-term fix is to define a struct kinfo_tty containing only the non-pointer fields pstat(8) needs and copy those out.

References

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/tty.c.
  • pending Reported to DragonFlyBSD security contact.

PoC verification

Evidence pack

findings/poc/DF-0006 ยท 12 files
FileTypeDescriptionSize
leak_ttys.c trigger-source reads kern.ttys blob, scans + dumps kernel pointers and raw blob 3.5 KB view raw
build.sh build-script cc -o leak_ttys leak_ttys.c 155 B view raw
run.sh run-script runs leak_ttys as unprivileged user 207 B view raw
build.log build-log final successful build, full output 67 B view raw
run.log run-log decisive run: 102 kernel pointers leaked 3.2 KB view raw
run.2.log run-log 2nd run (variance): identical 102 pointers 3.2 KB view raw
run.3.log run-log 3rd run (variance): identical 102 pointers 3.2 KB view raw
leak_sample.txt leak-sample leaked function/heap pointers + exact nm /boot/kernel/kernel cross-ref 3.2 KB view raw
VERDICT.md verdict full mechanism, gate trace, proof, impact 4.8 KB โ†“ raw
README.md readme build/run/expected for humans 2.3 KB โ†“ raw
fix.diff suggested-fix git-apply-able: zero all pointer fields of local copy before SYSCTL_OUT (tty.c:2911) 662 B view raw
env.txt environment guest uname, cc version, sysctls 385 B view raw
README.md readme build/run/expected for humans
โ†“ download raw

DF-0006 โ€” PoC

leak_ttys.c โ€” unprivileged leak of kernel function/heap pointers via the kern.ttys sysctl.

The issue

sysctl_kern_ttys() (sys/kern/tty.c:2891-2921) emits each struct tty verbatim:

t = *tp;                                  /* tty.c:2911  whole struct */
if (t.t_dev) t.t_dev = devid_from_dev(..);/* only t_dev sanitized */
SYSCTL_OUT(req, &t, sizeof(t));           /* tty.c:2914 */

struct tty (sys/sys/tty.h) carries kernel .text function pointers (t_oproc/t_stop/t_param/t_unhold โ†’ exact KASLR-defeat), kernel heap object pointers (t_pgrp/t_session/t_sigio/t_sc/t_slsc), per-clist c_data buffer pointers, and an embedded lwkt_token. Only t_dev is sanitized. The OID is CTLTYPE_OPAQUE|CTLFLAG_RD (tty.c:2923) and sysctl reads are not privilege-gated (kern_sysctl.c:1446-1450 gates only writes), so any unprivileged local user can dump them all.

Build

cc -o leak_ttys leak_ttys.c      # or: ./build.sh

Run

As an unprivileged user (e.g. maxx, uid 1001):

./leak_ttys       # or: ./run.sh

Expected output (bug present)

got 3760 bytes from kern.ttys
raw blob written to ttys.bin (3760 bytes)
  blob offset   256 (word    32): 0xffffffff80b8b800   <- t_oproc = scstart
  blob offset   264 (word    33): 0xffffffff806b7eb0   <- t_stop  = nottystop
  blob offset   272 (word    34): 0xffffffff80b86930   <- t_param = scparam
  ...
total kernel-range pointer-sized values leaked: 102

The 0xffffffff80???????? values match nm /boot/kernel/kernel symbols exactly (see leak_sample.txt) โ€” proving real kernel .text addresses are leaked and KASLR is defeated. The 0xfffff8?????????? values are live slab / direct-map heap addresses (clist buffers, pgrp/session objects). Stable across runs in the same boot (run.log/run.2.log/run.3.log). On a fixed kernel, zero kernel-range pointers appear and the program exits 2.

Cross-referencing against nm (optional, reproduces the leak_sample.txt table)

nm -n /boot/kernel/kernel > kernel.nm        # world-readable
./leak_ttys                                  # writes ttys.bin
# then for each candidate value, find the nearest nm symbol:
python3 -c "import bisect,struct; ..."       # see leak_sample.txt for results
VERDICT.md verdict full mechanism, gate trace, proof, impact
โ†“ download raw

DF-0006 โ€” kern.ttys sysctl leaks kernel function/heap pointers to unprivileged users

Verdict

REPRODUCED โ€” kern.ttys is world-readable and copies each struct tty verbatim to userland, leaking 102 kernel-range pointer-sized values per read, including exact .text function-pointer addresses that defeat KASLR. Confirmed on DragonFly master DEV v6.5.0.1712.g89e6a-DEVELOPMENT.

Mechanism (confirmed by source + run)

sysctl_kern_ttys (sys/kern/tty.c:2891-2921) iterates the global tty_list and, for each struct tty, does:

t = *tp;                                      /* tty.c:2911  whole-struct copy */
if (t.t_dev)
    t.t_dev = (cdev_t)(uintptr_t)devid_from_dev(t.t_dev);   /* ONLY t_dev sanitized */
error = SYSCTL_OUT(req, (caddr_t)&t, sizeof(t));            /* tty.c:2914 */

The OID is registered CTLTYPE_OPAQUE|CTLFLAG_RD (tty.c:2923-2924) โ€” a plain read. sysctl_root (sys/kern/kern_sysctl.c:1446-1450) applies its privilege check (caps_priv_check(.., SYSCAP_NOSYSCTL_WR)) only when req->newptr is set (a write). A read sets no newptr, so no privilege check runs; any unprivileged user reads kern.ttys.

struct tty (sys/sys/tty.h) carries, all copied raw: - .text function pointers t_oproc / t_stop / t_param / t_unhold (lines 94-99) - kernel heap pointers t_pgrp (86), t_session (87), t_sigio (88), t_sc (100), t_slsc (101) - per-clist data-buffer pointers t_rawq.c_data / t_canq.c_data / t_outq.c_data (line 50) - an embedded lwkt_token t_token (74)

Only t_dev (82) is rewritten before copyout.

Proof

./leak_ttys run as maxx (uid 1001, not in wheel) returns 3760 bytes (10 ttys ร— 376-byte struct tty) and scans 102 kernel-range pointers. Cross-referencing the leaked .text values against nm /boot/kernel/kernel (also world-readable) yields exact symbol matches:

intra-off field (struct tty) leaked value nm symbol match
256 t_oproc 0xffffffff80b8b800 scstart EXACT
264 t_stop 0xffffffff806b7eb0 nottystop EXACT
272 t_param 0xffffffff80b86930 scparam EXACT
256 t_oproc (com) 0xffffffff80c2e9b0 comstart EXACT
264 t_stop (com) 0xffffffff80c2fe30 comstop EXACT
272 t_param (com) 0xffffffff80c30120 comparam EXACT

Plus heap pointers in the 0xfffff8xxxxxxxxxx direct-map region (clist c_data buffers, t_pgrp/t_session slab objects, the global token at intra-off 352). Stable across 3 reads in the same boot (see run.log/run.2.log/run.3.log).

Because the leaked function-pointer values equal the static nm addresses, this build has no KASLR slide on the tty ops โ€” an attacker recovers the exact kernel .text base and heap layout from an unprivileged account.

Impact

Information disclosure: precise kernel .text base + kernel heap layout to any local unprivileged user. Not a memory-corruption primitive itself, but it is a KASLR-defeat and slab-grooming enabler that escalates the practical severity of any local heap/stack corruption bug into reliable exploitation. Rated Low standalone (info disclosure, no integrity/availability impact).

Is the node world-readable? (privilege-gate trace)

Yes. The OID at sys/kern/tty.c:2923 is CTLFLAG_RD with no CTLFLAG_ANYBODY needed (that flag is write-only). sysctl_root (kern_sysctl.c:1446-1450) gates only writes:

if (!(oid->oid_kind & CTLFLAG_ANYBODY) && req->newptr && p &&
    (error = caps_priv_check(td->td_ucred, SYSCAP_NOSYSCTL_WR)))
    return (error);

req->newptr is NULL on reads, so the branch is skipped. kern.ttys is readable by maxx (verified). No per-handler privilege check exists in sysctl_kern_ttys. โ†’ the gate that would prevent the leak is absent on master.

PoC changes

Rewrote leak_ttys.c: the original used the wrong kernel-address range (>= 0xffffffff80000000) which misses the DragonFly direct-map/heap region (0xfffff8xxxxxxxxxx) where most of the pointers live, and printed only a subset. The new version detects the full canonical kernel upper half (>= 0xffff800000000000), prints up to 60 candidates, and dumps the raw blob to ttys.bin so the leaked function pointers can be cross-referenced against nm /boot/kernel/kernel (done in leak_sample.txt). Added build.sh/run.sh.

Matches the finding markdown's proposal. Authoritative fix.diff in this folder zeros all pointer fields of the local copy before SYSCTL_OUT while preserving t_dev sanitization and every field pstat(8) reads. A stronger long-term fix is a dedicated pointer-free struct kinfo_tty (mirroring kinfo_file).

Confirmed kernel references

Detail

Exploit chain

none (pure info-disclosure / KASLR-defeat primitive; no memory corruption to chain). The leaked .text base + slab layout is an ENABLER that would escalate a separate local heap-corruption bug (e.g. DF-0013) from DoS to reliable LPE by defeating KASLR and revealing slab layout for grooming. No further primitive derivable from this read alone.

Evidence (decisive lines)

got 3760 bytes from kern.ttys
  blob offset   256 (word    32): 0xffffffff80b8b800   <- t_oproc = scstart  (.text, nm EXACT)
  blob offset   264 (word    33): 0xffffffff806b7eb0   <- t_stop  = nottystop (.text, nm EXACT)
  blob offset   272 (word    34): 0xffffffff80b86930   <- t_param = scparam  (.text, nm EXACT)
total kernel-range pointer-sized values leaked: 102
RUN_EXIT=0  (read as uid=1001 maxx, not in wheel)

PoC changes

Rewrote leak_ttys.c: original used the wrong kernel-address range (>=0xffffffff80000000) which misses DragonFly's 0xfffff8xxxxxxxxxx direct-map/heap region where most struct-tty pointers live; fixed to detect the full canonical kernel upper half (>=0xffff800000000000), prints up to 60 candidates, and dumps the raw blob to ttys.bin for nm cross-reference. Added build.sh/run.sh. Full packs in findings/poc/DF-0006/ (VERDICT.md, leak_sample.txt w/ nm cross-ref, run.log+run.2.log+run.3.log, fix.diff).

Verified recommended fix

fix.diff (git apply --check OK) zeros all pointer fields of the local copy before SYSCTL_OUT in sysctl_kern_ttys (tty.c:2912): bzero t_token, NULL t_pgrp/t_session/t_sigio/t_sc/t_slsc, the three c_data buffers, and the four function pointers t_oproc/t_stop/t_param/t_unhold, preserving t_dev sanitization and every field pstat(8) reads. Matches finding markdown proposal; stronger long-term fix is a dedicated pointer-free struct kinfo_tty (mirroring kinfo_file).

Verdict

REPRODUCED. sysctl_kern_ttys (sys/kern/tty.c:2891-2921) does t = *tp; (whole-struct copy at :2911), sanitizes ONLY t_dev (:2912-2913), then SYSCTL_OUT(&t, sizeof(t)) (:2914); the OID is CTLFLAG_RD (:2923) and sysctl reads are not framework-gated (kern_sysctl.c:1446-1450 checks only writes), so unprivileged maxx reads kern.ttys and gets 3760 bytes (10 ttys x 376B struct tty) containing 102 kernel-range pointers per run. The leaked .text function-pointer values match nm /boot/kernel/kernel EXACTLY: intra-offset 256=t_oproc=0xffffffff80b8b800=scstart, 264=t_stop=0xffffffff806b7eb0=nottystop, 272=t_param=0xffffffff80b86930=scparam (and comstart/comstop/comparam for the serial tty) -> precise KASLR defeat; the 0xfffff8xxxxxxxxxx values are live clist c_data buffers, t_pgrp/t_session slab objects, and the global token (heap-layout disclosure). Stable across 3 runs (run.log/run.2.log/run.3.log). No code change on master closes this.