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
.textaddresses via the function pointers (for ptyst_oproc/t_stop/t_unholdpoint toptsstart/ptsstop/ptsunholdintty_pty.c) โ a precise KASLR-relocation primitive; (b) kernel heap addresses (pgrp,session,sigio, per-queuec_databuffers) 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.
Recommended fix
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
sys/kern/tty.c:2891-2921โsysctl_kern_ttysraw copyout.sys/sys/tty.hโstruct ttypointer fields.sys/kern/kern_descrip.cโkinfo_filesanitized-export pattern.- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor.
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| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
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.
Recommended fix
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.