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

snp: snoop line discipline installable via unprivileged TIOCSETD; snplclose NULL-derefs t_sc on tty close (kernel panic)

Summary

TIOCSETD accepts any discipline index with no privilege check, and the snoop discipline's l_open is plain ttyopen which never sets t_sc - closing such a tty runs snplclose which does snp_down(NULL), faulting at offset 0x30. Any local user triggers it on a private pty once snp.ko is loaded; /dev/snp perms irrelevant (device never opened). REPRODUCED on guest as uid 1001 (Fatal trap 12, fault VA 0x30, Stopped at snp_down+0x73) and FIX-VALIDATED. Guard-t_sc-NULL fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4668 Β· 10 files
FileTypeDescriptionSize
snp_disc.c β€” 2.3 KB view raw
build.sh β€” 101 B view raw
run.sh β€” 140 B view raw
run.log β€” 2.1 KB view raw
panic.txt β€” 1.6 KB view raw
env.txt β€” 424 B view raw
VERDICT.md β€” 3.1 KB ↓ raw
fix.diff β€” 1.5 KB view raw
manifest.json β€” 796 B view raw
verdict.json β€” 3.2 KB view raw
VERDICT.md
↓ download raw

DF-4668 β€” VERDICT

Status: reproduced. Impact: panic (unprivileged local kernel DoS). Confidence: certain.

What was run

  • Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026, kernel X86_64_GENERIC, QEMU/KVM (dfbsd-qemu), fresh vm.sh reset with-src.
  • Setup (root): kldload snp (module shipped at /boot/kernel/snp.ko; /dev/snp* created 0600 root:wheel β€” trigger does NOT touch those nodes).
  • Build (unpriv user maxx, uid 1001, not in wheel): cc -O1 -o /tmp/snp_disc /tmp/snp_disc.c -lutil β€” clean.
  • Run: /tmp/snp_disc as maxx. ssh connection died mid-run; vm.sh status => down; serial console (vm.sh log) shows the panic below.

Decisive evidence (panic.txt)

Fatal user address access from kernel mode from snp_disc at ffffffff826005b3
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0x30
fault code             = supervisor read data, page not present
Stopped at      snp_down+0x73:  cmpq    $0x1000,0x30(%rbx)

0x30 = 48 = offsetof(struct snoop, snp_blen) (list 16 + target 8 + tty 8 + len 8 + base 8); $0x1000 = SNOOP_MINLEN. The faulting instruction is literally snp.c:517 (if (snp->snp_blen != SNOOP_MINLEN)) with snp == tp->t_sc == NULL, reached from snplclose on tty close.

Why it reproduces (path:line trace)

  1. sys/kern/tty_pty.c:406 β€” pty slave close calls (*linesw[tp->t_line].l_close)(tp, flag) β†’ snplclose when the discipline is the snoop one.
  2. sys/kern/tty.c:1128-1145 β€” TIOCSETD accepts ANY t < nlinesw with no privilege check (nlinesw = MAXLDISC = 9, sys/kern/tty_conf.c:79).
  3. sys/kern/tty_conf.c:93-115 β€” ldisc_register(LDISC_LOAD, &snpdisc) from snp_modevent (snp.c:721) installs snoop in the last free slot 7..8; its l_open (ttyopen, snp.c:69) never sets tp->t_sc, so t_sc stays NULL.
  4. sys/dev/misc/snp/snp.c:135-137 β€” snplclose does snp = tp->t_sc; error = snp_down(snp); with no NULL check.
  5. sys/dev/misc/snp/snp.c:517 β€” snp_down(NULL) reads snp->snp_blen β†’ fatal page fault at 0x30.

PoC fixes vs the seed sketch: the enumeration must distinguish "l_open failed" (nodisc slots return error) from "beyond nlinesw" (ENXIO) β€” slots 1..6 fail in l_noopen, slot 7/8 (snoop, ttyopen) succeed; TIOCGETD confirms the install.

Impact ceiling

Pure NULL-pointer read at a fixed kernel address β†’ deterministic panic from any local user once the snp module is loaded (admin ran watch(1)/kldload). No corruption, no info leak β€” a reliable unprivileged DoS. Not reachable to privesc (the faulting data is a constant NULL).

Fix validation

fix.diff (generated against read-only sys/, applied to the guest's /usr/src copy, kernel rebuilt with make nativekernel): re-running the exact PoC β€” see verdict.json fix_* fields and run.2.log in this pack.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src, kernel rebuilt (make nativekernel RC=0) and installed (#1 Tue Sep 22 18:38:10 UTC 2026), snp.ko loaded; exact same PoC run as unprivileged maxx: 9 line disciplines found, snoop-disc pty slave closed, no panic, exit=0, guest up. Baseline panic vs patched survive both captured (panic.txt / run.2.log).

run.2.log (patched run output); panic.txt (baseline); fix.diff; build.log
↓ fix.diffper-fix-DF-4668

Confirmed kernel references

Detail

Exploit chain

unprivileged user (uid 1001, no groups) -> TIOCSETD(own pty, disc 8=snooplinedisc) -> close(slave) -> ptsclose -> snplclose with t_sc==NULL -> snp_down(NULL) -> deterministic kernel panic (DoS). No privesc: the faulting pointer is a constant NULL.

Evidence (decisive lines)

panic.txt: 'fault virtual address = 0x30' + 'Stopped at snp_down+0x73: cmpq $0x1000,0x30(%rbx)' (offset 0x30 = offsetof(struct snoop, snp_blen); $0x1000 = SNOOP_MINLEN). run.2.log: same trigger on patched kernel survives.

PoC changes

Seed sketch fixed: (1) enumeration must distinguish l_open failure (nodisc slots 1-6) from ENXIO (t >= nlinesw) and confirm install via TIOCGETD - snp occupies slot 7/8, so the highest installing index is snooplinedisc; (2) include (no on DFly) and link -lutil; (3) keep every switched pty open until the final close (closing earlier IS the trigger).

Verified recommended fix

snplclose: if tp->t_sc == NULL (discipline installed via TIOCSETD without SNPSTTY attach), skip snp_down and just run ttylclose under tp->t_token.

Verdict

Unprivileged kernel NULL-deref panic reproduced on the stock 6.5-DEVELOPMENT guest: with snp.ko loaded, any user installs the snoop line discipline on a private pty via TIOCSETD (tty.c:1128 has no privilege check; ttyopen() never sets t_sc), and closing the tty runs snplclose -> snp_down(NULL) -> read at NULL+0x30 (snp_blen). Fatal trap 12, 'Stopped at snp_down+0x73: cmpq $0x1000,0x30(%rbx)'. Fixed kernel rebuilt with the guard in snplclose: identical run survives, exit=0, guest stays up.