sysctl kern.ttys leaks raw kernel pointers (function + heap) to unprivileged users
Summary
kern.ttys sysctl handler copies each struct tty verbatim (t=*tp) and exports to userspace sanitizing only t_dev. Every other pointer field - clist data buffers driver function pointers (t_oproc/t_stop/t_param/t_unhold) and heap objects (t_session/t_pgrp/t_sigio/t_sc/t_slsc/kqinfo lists) disclosed in clear. Node CTLFLAG_RD sysctl_root gates writes not reads so any unprivileged local user can read it. Defeats kernel ASLR leaks heap layout. t_rawq/t_canq/t_outq each clist whose short *c_data is live kernel heap pointer. t_oproc kernel text function pointer. Any single non-NULL t_oproc reveals address of driver routine defeating KASLR.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2548 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | sysctl(2) reader; scans kern.ttys blob for kernel-range pointers, splits .text vs heap | 6.4 KB | view raw |
| build.sh | build-script | cc -O2 -o poc poc.c | 116 B | view raw |
| run.sh | run-script | ./poc as unprivileged user | 120 B | view raw |
| README.md | readme | human-facing build/run/expected + file index | 2.8 KB | β raw |
| VERDICT.md | verdict | full narrative: mechanism, proof, impact, fix, fix-validation before/after | 7.7 KB | β raw |
| build.log | build-log | final successful PoC build (cc, exit 0) | 13 B | view raw |
| run.log | run-log | decisive baseline run: 102 ptrs leaked (38 .text + 64 heap) | 2.6 KB | view raw |
| run.2.log | run-log | variance run 2: 102 ptrs (deterministic) | 2.6 KB | view raw |
| run.3.log | run-log | variance run 3: 102 ptrs (deterministic) | 2.6 KB | view raw |
| fix_run.log | run-log | patched-kernel (#1) run: 0 ptrs leaked | 399 B | view raw |
| fix_build.log | build-log | single-fix kernel build (make -j6 nativekernel, rc=0) | 5.6 MB | β download |
| fix.diff | suggested-fix | git-apply-able: bzero/NULL all kernel-pointer fields of struct tty before SYSCTL_OUT | 1.7 KB | view raw |
| leak_sample.txt | leak-sample | raw leaked pointers (3-run determinism) + .text cross-reference vs nm | 4.0 KB | view raw |
| env.txt | environment | uname, kern.version, cc version, sysctl OID description | 671 B | view raw |
| ttys.bin | leak-sample | raw 3760-byte kern.ttys blob from unpatched baseline | 3.7 KB | β download |
| manifest.json | manifest | this file | 3.1 KB | view raw |
DF-2548 β kern.ttys sysctl pointer leak (re-verification)
DF-2548 is a re-verification run of the kern.ttys sysctl pointer-leak
finding (filed originally as DF-0006 at the same source location). The bug
remains present and unpatched in current DragonFlyBSD master DEV.
Claim
sysctl_kern_ttys (sys/kern/tty.c:2891-2921) copies each struct tty
verbatim (t = *tp; at tty.c:2911) and exports it raw to userspace via
SYSCTL_OUT(req, &t, sizeof(t)) (tty.c:2914). Only t_dev is sanitized
(tty.c:2912-2913). Every other pointer field β driver function pointers
(t_oproc/t_stop/t_param/t_unhold), heap object pointers
(t_session/t_pgrp/t_sigio/t_sc/t_slsc), clist data-buffer pointers,
the embedded lwkt_token (t_ref/t_desc), the embedded kqinfo
(t_rkq/t_wkq ki_note), and the t_list TAILQ linkage β is leaked raw
to any unprivileged local sysctl reader.
Build & run
cc -O2 -o poc poc.c # build.sh ./poc # run.sh β run as an UNPRIVILEGED user
Expected output
Bug present (unpatched #0 baseline):
got 3760 bytes from kern.ttys (readable as UNPRIVILEGED user) ... blob off 256 TEXT/FN : 0xffffffff80b8c0f0 blob off 264 TEXT/FN : 0xffffffff806b87a0 blob off 272 TEXT/FN : 0xffffffff80b87220 ... TOTAL kernel-range pointers leaked : 102 of which kernel .text/.rodata (FN) : 38 of which heap/direct-map : 64 VERDICT: LEAK CONFIRMED
Exit code 0.
Fixed kernel (#1):
got 3760 bytes from kern.ttys (readable as UNPRIVILEGED user) ... TOTAL kernel-range pointers leaked : 0 of which kernel .text/.rodata (FN) : 0 of which heap/direct-map : 0 VERDICT: NO LEAK β pointer fields are sanitized (fixed kernel).
Exit code 2.
Preconditions
- Any local user (uid 1001
maxx, not inwheel, used in this verification). - No sysctl, mount, module, or privilege required β
kern.ttysis a plainCTLFLAG_RDOID andsysctl_rootdoes not privilege-gate reads.
Files
| file | what |
|---|---|
poc.c |
minimal sysctl(2) reader; scans blob for kernel-range pointers |
build.sh |
cc -O2 -o poc poc.c |
run.sh |
./poc as the unprivileged user |
build.log |
final successful build output |
run.log, run.2.log, run.3.log |
3 consecutive baseline runs (deterministic 102 ptrs) |
fix_run.log |
patched-kernel run (0 pointers) |
fix_build.log |
single-fix kernel build output (make nativekernel, rc=0) |
fix.diff |
git-apply-able unified diff sanitizing all pointer fields |
leak_sample.txt |
raw leaked pointers + cross-reference + 3-run variance |
env.txt |
guest uname, cc version, sysctl OID description |
ttys.bin |
raw 3760-byte kern.ttys blob (unpatched baseline) |
VERDICT.md |
full narrative analysis |
manifest.json |
machine-readable catalog |
DF-2548 β VERDICT
Verdict: REPRODUCED + FIX VALIDATED
The kern.ttys sysctl handler leaks raw kernel pointers (function pointers +
heap object pointers + clist data-buffer pointers + lwkt_token/kqinfo internals)
to any unprivileged local reader. The leak was reproduced on the unpatched
audit-source kernel (6.5-DEVELOPMENT #0) and the authored fix.diff
eliminated every leaked pointer on a single-fix kernel (6.5-DEVELOPMENT #1).
Mechanism (confirmed line-by-line)
sysctl_kern_ttys (sys/kern/tty.c:2891-2921):
- Walks the global
tty_listundertty_token(tty.c:2903-2906). - For each
struct tty *tp, makes a whole-struct copy on the stack:t = *tp;(tty.c:2911). - Sanitizes only
t_devviadevid_from_dev(tty.c:2912-2913) β turns thecdev_tinto a small integer device id. - Emits the entire 376-byte
struct ttyraw:error = SYSCTL_OUT(req, (caddr_t)&t, sizeof(t));(tty.c:2914).
Every other pointer-bearing field in struct tty (sys/sys/tty.h:73-114) is
copied verbatim:
| field(s) | type | what leaks |
|---|---|---|
t_token |
struct lwkt_token |
t_ref, t_desc pointers (sys/sys/thread.h:159-164) |
t_rawq/t_canq/t_outq .c_data |
short * (Γ3) |
clist data-buffer heap addresses |
t_pgrp / t_session / t_sigio |
struct pgrp * / session * / sigio * |
kernel-heap object addresses |
t_rkq / t_wkq |
struct kqinfo (Γ2) |
ki_note SLIST pointer (sys/sys/event.h:160-162) |
t_oproc / t_stop / t_param / t_unhold |
function pointers (Γ4) | kernel .text addresses (driver ops vector) |
t_sc / t_slsc |
void * (Γ2) |
driver softc / line-disc softc |
t_list |
TAILQ_ENTRY(tty) |
tqe_next, tqe_prev linkage pointers |
The OID is SYSCTL_PROC(_kern, OID_AUTO, ttys, CTLTYPE_OPAQUE|CTLFLAG_RD,
..., "S,tty", ...) (tty.c:2923-2924). sysctl_root (in
kern_sysctl.c) applies its privilege/securelevel checks only when
req->newptr is set (writes), so a plain read sets no newptr and any
local user β no group, no privilege β can read kern.ttys.
Proof (unpatched #0 baseline)
Run as maxx (uid 1001, not in wheel):
$ id uid=1001(maxx) gid=1001(maxx) groups=1001(maxx) $ ./poc got 3760 bytes from kern.ttys (readable as UNPRIVILEGED user) ... blob off 24 TEXT/FN : 0xffffffff80c64d8d blob off 48 HEAP/DM : 0xfffff801168fb400 blob off 256 TEXT/FN : 0xffffffff80b8c0f0 <- t_oproc (intra-record off 256) blob off 264 TEXT/FN : 0xffffffff806b87a0 <- t_stop (near ttyread 0xffffffff806b87b0) blob off 272 TEXT/FN : 0xffffffff80b87220 <- t_param blob off 360 TEXT/FN : 0xffffffff810e5200 <- t_unhold ... TOTAL kernel-range pointers leaked : 102 of which kernel .text/.rodata (FN) : 38 of which heap/direct-map : 64 VERDICT: LEAK CONFIRMED
- Blob = 3760 bytes = 10
struct ttyrecords Γ 376 bytes/record. - 102 raw kernel pointer-sized values per read.
- 38 are in kernel
.text/.rodata(0xffffffff8xxxxxxx): driver function pointers β directly KASLR-defeating. The same address repeats at the same intra-record offset across records because the same TTY driver ops vector is used (e.g.t_oproc=0xffffffff80b8c0f0at intra-record off 256 in every pty record). - 64 are in the heap/direct-map range (
0xfffff80xxxxxxxxx/0xfffff800xxxxxxxxx): clist data buffers, pgrp/session/sigio objects, softc pointers, kqinfo lists β reveals kernel heap layout for grooming. - Deterministic: 3 consecutive runs leaked byte-identical 102 pointers (no residue variance β these are live struct field values, not stack junk). This is worse than a residue leak: a single read pins down KASLR .text base + multiple fixed heap addresses.
Impact
Pure information disclosure (addresses, not arbitrary memory contents).
- Defeats KASLR: 38 leaked .text addresses per read let an attacker compute
the kernel text base and resolve any symbol offline (nm
/boot/kernel/kernel).
- Reveals kernel heap layout (clist buffers, pgrp/session/sigio/softc objects)
usable to refine slab grooming for a separate heap-corruption bug.
- No primitive derivable from this alone (read-only sysctl); this is the valid
hard-blocker ceiling for the uid=0 chain. The leak itself is the finding.
Severity: Medium (unprivileged KASLR-defeat + heap-layout leak on default kernel; no privilege boundary crossed by the read itself, but it materially lowers the bar for exploiting any future local kernel memory-corruption bug).
Why this is the same defect as DF-0006
DF-0006 (findings/DF-0006-kern-ttys-sysctl-pointer-leak.md) filed the
identical bug at the same source lines (tty.c:2891-2921). DF-2548 is a
re-verification run against current master DEV that re-confirms the bug is
still present and unpatched, re-characterizes the leak (102 pointers, 38 of
which are KASLR-defeating .text), and re-validates the same field-sanitization
fix (extended to also clear t_rkq/t_wkq kqinfo and the t_list TAILQ
linkage, which DF-0006's verified fix already covered).
Fix (fix.diff)
After the existing t_dev devid rewrite, zero every kernel-pointer-bearing
field of the local copy before SYSCTL_OUT:
bzero(&t.t_token, sizeof(t.t_token))β clearst_ref,t_desc.t.t_rawq/t_canq/t_outq .c_data = NULLβ clist data buffers.t.t_pgrp / t_session / t_sigio = NULLβ heap object pointers.bzero(&t.t_rkq / t_wkq, sizeof(...))β kqinfoki_noteSLIST pointers.t.t_oproc / t_stop / t_param / t_unhold = NULLβ driver.textfunction pointers.t.t_sc / t_slsc = NULLβ driver/line-disc softc.t.t_list.tqe_next / tqe_prev = NULLβ global list linkage.
The remaining exported fields (termios, winsize, watermarks, t_state,
t_flags, t_column, t_line, t_timeout, t_gen, t_refs, clist
c_cc/c_ccmax/c_cchead counts, t_rawcc/t_cancc/t_outcc) are pure
scalars β these are what pstat(8) actually consumes, so the ABI it depends
on is preserved.
A stronger long-term fix is to define a struct kinfo_tty containing only the
non-pointer fields pstat(8) needs (mirroring the kinfo_file pattern in
kern_descrip.c) β but the in-place sanitization above is the minimal,
targeted root-cause fix and is what this verification validated.
Fix validation (Phase 8)
| kernel | kern.version |
leaked ptrs (per PoC) | result |
|---|---|---|---|
unpatched baseline (with-src, INVARIANTS ON) |
6.5-DEVELOPMENT #0 Thu Jul 2 |
102 (38 .text + 64 heap) | LEAK |
| single-fix kernel | 6.5-DEVELOPMENT #1 Sat Aug 8 (sha256 99e035c1...) |
0 (3/3 runs) | FIXED |
Clean before/after: same PoC, same unprivileged user, same 3760-byte sysctl
blob β only the pointer fields differ (raw on #0, all NULL/0 on #1).
PoC changes
The PoC was authored from scratch for DF-2548 (poc.c) because the
findings/poc/DF-2548/ directory was empty. It is functionally equivalent to
DF-0006's leak_ttys.c (reads kern.ttys via sysctlbyname(2), scans every
8-byte slot for canonical-upper-half kernel addresses, splits .text vs
heap/direct-map, dumps raw blob to ttys.bin), with sharper output
(per-pointer TEXT/FN vs HEAP/DM labelling, structured totals, exit code 0 on
leak / 2 on no-leak).
How to reproduce
ssh dfbsd-maxx # as the unprivileged user cd ~/poc/DF-2548 ./build.sh # cc -O2 -o poc poc.c ./run.sh # exit 0 = leak present; exit 2 = no leak (fixed)
Fix verification
fixedVALIDATED. Applied fix.diff (single hunk, git apply --check + patch --dry-run clean) to /usr/src/sys/kern/tty.c on with-src baseline, built 'make -j6 nativekernel KERNCONF=X86_64_GENERIC' (rc=0), installed kernel.stripped/kernel.debug to /boot/kernel/, rebooted to kern.version #1 (Sat Aug 8 18:32:33). SAME ./poc as unprivileged uid 1001 maxx that leaked 102 raw kernel pointers (38 .text + 64 heap) on unpatched #0 now leaks 0 pointers across 3/3 runs, while sysctl still returns same 3760-byte blob (only pointer fields differ: raw on #0, NULL/0 on #1). Before/after clean and deterministic. Fix closes the bug.
BASELINE (unpatched #0, Thu Jul 2): TOTAL kernel-range pointers leaked 102 (38 .text/.rodata + 64 heap/direct-map); 3/3 runs byte-identical. Sample t_oproc=0xffffffff80b8c0f0, t_stop=0xffffffff806b87a0, t_param=0xffffffff80b87220, t_unhold=0xffffffff810e5200, plus clist/heap 0xfffff801168fb400 etc. PATCHED (single-fix #1, Sat Aug 8 18:32:33, sha256 99e035c1...): TOTAL 0 (0 .text + 0 heap); 3/3 runs 0. Sysctl still returns 3760 bytes (10 struct tty records) β only pointer fields now NULL.
Confirmed kernel references
- sys/kern/tty.c:2891
- sys/kern/tty.c:2906
- sys/kern/tty.c:2911
- sys/kern/tty.c:2912
- sys/kern/tty.c:2913
- sys/kern/tty.c:2914
- sys/kern/tty.c:2923
- sys/sys/tty.h:73
- sys/sys/tty.h:74
- sys/sys/tty.h:86
- sys/sys/tty.h:87
- sys/sys/tty.h:88
- sys/sys/tty.h:94
- sys/sys/tty.h:96
- sys/sys/tty.h:98
- sys/sys/tty.h:99
- sys/sys/tty.h:100
- sys/sys/tty.h:101
- sys/sys/thread.h:159
- sys/sys/event.h:160
Detail
Exploit chain
none β pure information disclosure (read-only sysctl). No write/corruption primitive derivable from a sysctl read, valid hard-blocker ceiling per Phase 6. The leak itself is the finding: directly defeats KASLR (38 .text addresses per read compute kernel text base and resolve any symbol offline) and reveals kernel heap layout (clist buffers, pgrp/session/sigio/softc objects) usable to refine slab grooming for a separate heap-corruption bug. No escalation chain attempted because primitive is read-only by construction.
Evidence (decisive lines)
BASELINE (#0 unpatched, uid 1001 maxx): blob off 256 TEXT/FN 0xffffffff80b8c0f0 (t_oproc); off 264 0xffffffff806b87a0 (t_stop); off 272 0xffffffff80b87220 (t_param); off 360 0xffffffff810e5200 (t_unhold); off 48 HEAP/DM 0xfffff801168fb400 (clist/heap). TOTAL kernel-range pointers leaked: 102 (of which .text/.rodata 38, heap/direct-map 64). 3/3 runs byte-identical (deterministic). FIXED (#1 single-fix kernel): TOTAL kernel-range pointers leaked: 0 (3/3 runs = 0).
PoC changes
findings/poc/DF-2548/ was empty; authored poc.c from scratch (sysctl(2) reader that scans kern.ttys blob at every 8-byte stride for canonical-upper-half kernel addresses, splits .text vs heap/direct-map, dumps raw blob to ttys.bin, exits 0 on leak / 2 on no-leak), build.sh, run.sh, README.md, VERDICT.md, manifest.json, fix.diff. Functionally equivalent to DF-0006's leak_ttys.c with sharper TEXT/FN vs HEAP/DM labelling.
Verified recommended fix
In sysctl_kern_ttys (sys/kern/tty.c), immediately after existing t_dev devid rewrite (tty.c:2912-2913) and before SYSCTL_OUT (tty.c:2914), zero every kernel-pointer-bearing field of the local struct tty copy: bzero(&t.t_token,...), NULL the three clist c_data pointers, NULL t_pgrp/t_session/t_sigio, bzero(&t.t_rkq)/bzero(&t.t_wkq), NULL t_oproc/t_stop/t_param/t_unhold, NULL t_sc/t_slsc, NULL t_list.tqe_next/tqe_prev. Remaining exported fields (termios, winsize, watermarks, t_state, t_flags, etc.) are pure scalars and what pstat(8) consumes β ABI preserved. Matches DF-0006 verified fix (extended to t_rkq/t_wkq kqinfo and t_list linkage). Stronger long-term fix: pointer-free struct kinfo_tty mirroring kern_descrip.c kinfo_file pattern. Full git-apply-able diff in findings/poc/DF-2548/fix.diff.
Verdict
REPRODUCED then FIX VALIDATED. sysctl_kern_ttys (sys/kern/tty.c:2891-2921) copies each struct tty verbatim to a local (t = *tp at tty.c:2911) and emits it raw via SYSCTL_OUT (tty.c:2914); only t_dev is rewritten to a small devid (tty.c:2912-2913). Every other pointer-bearing field is leaked: driver .text function pointers t_oproc/t_stop/t_param/t_unhold, heap object pointers t_pgrp/t_session/t_sigio/t_sc/t_slsc, clist c_data buffers, embedded lwkt_token (t_ref/t_desc) and kqinfo (ki_note), and t_list TAILQ linkage. OID is CTLTYPE_OPAQUE|CTLFLAG_RD and sysctl reads NOT privilege-gated, so uid 1001 reads it freely. Confirmed by sysctlbyname('kern.ttys') returning a 3760-byte blob containing 102 kernel-range pointer-sized values per call (38 in kernel .text/.rodata 0xffffffff8xxxxxxx = KASLR-defeating, 64 in heap/direct-map). Deterministic (byte-identical across 3 runs) β single read pins KASLR base + multiple fixed heap addresses. Same defect as DF-0006, re-verified against current master DEV (still unpatched).
No comments yet.