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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2548 Β· 16 files
FileTypeDescriptionSize
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
README.md readme human-facing build/run/expected + file index
↓ download 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 in wheel, used in this verification).
  • No sysctl, mount, module, or privilege required β€” kern.ttys is a plain CTLFLAG_RD OID and sysctl_root does 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
VERDICT.md verdict full narrative: mechanism, proof, impact, fix, fix-validation before/after
↓ download raw

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):

  1. Walks the global tty_list under tty_token (tty.c:2903-2906).
  2. For each struct tty *tp, makes a whole-struct copy on the stack: t = *tp; (tty.c:2911).
  3. Sanitizes only t_dev via devid_from_dev (tty.c:2912-2913) β€” turns the cdev_t into a small integer device id.
  4. Emits the entire 376-byte struct tty raw: 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 tty records Γ— 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=0xffffffff80b8c0f0 at 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)) β€” clears t_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(...)) β€” kqinfo ki_note SLIST pointers.
  • t.t_oproc / t_stop / t_param / t_unhold = NULL β€” driver .text function 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Aug 8 18:32:33 UTC 2026 (sha256 /boot/kernel/kernel = 99e035c1b45473db5638d5c7970b60b4383e186644e1ed26b3e4dbd8e62eccc0)

Confirmed kernel references

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