# 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)
```
