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

TIOCSCTTY reassignment orphans the old tty's t_session, yielding an unprivileged use-after-free of struct session (deterministic panic; ctty hijack of recycled sessions)

Field Value
ID DF-2687
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:H
CWE CWE-416 Use After Free (CWE-667 secondary)
File sys/kern/tty.c
Lines 1190-1199 (missing dissociation), 294-296 (ttyclearsession deref), 1623-1624 (ttymodem)
Area kern
Confidence certain
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

ttioctl() TIOCSCTTY (tty.c:1181-1204) moves s_ttyp to the new tty and ttyunhold()s the old one but never clears the old tty's t_session/t_pgrp β€” every other s_ttyp transition is paired with ttyclearsession (:279-316). Because the vnode layer (devfs_vnops.c:1598-1611) accepts a pty MASTER fd for TIOCSCTTY, making the master vnode s_ttyvp, the master's last close clears s_ttyvp via devfs' controlling-tty half-close (:1153-1156) without running ttyclose() (ptcclose never calls it), leaving tp->t_session set while the guard at :1183-1185 now passes (s_ttyvp == NULL). The leader then re-TIOCSCTTYs a second tty and exits; sess_rele() (kern_proc.c:930-947) repairs only sess->s_ttyp->t_session and kfrees the session, so the first tty's t_session dangles into freed M_SESSION memory. Closing the orphaned slave runs ttyclearsession β†’ lwkt_gettoken(&prg->proc_token) on freed memory (:294-296); ttymodem()'s ksignal(tp->t_session->s_leader, SIGHUP) (:1623-1624) is the same class. Additionally leaks a pgrp reference per trigger and enables ctty hijack: the freed 104-byte M_SESSION chunk is LIFO-recycled by the next setsid(), making the orphaned tty act as that unrelated session's controlling terminal (isctty matches; cross-session TIOCSPGRP/TIOCSWINSZ pgsignal reach).

Threat model & preconditions

Any local unprivileged user with access to /dev/ptmx. Reproduced: deterministic kernel panic (Fatal trap 9, lwkt_gettoken+0x64) from uid 1002 with debug.use_weird_array=1 making the freed-chunk read fault; without the flag the same unconditional deref silently consumes stale freed-memory contents. Ceiling beyond DoS: session-structure recycling gives attacker-influenced t_session targets (ksignal on chosen s_leader where ttymodem is reachable; cross-session signal delivery and job-control hijack), plus permanent kernel state corruption.

Proof of concept

findings/poc/DF-2687/df2687.c (unprivileged): open ptmx+pts; fork leader; parent drops master; leader setsid β†’ ioctl(master, TIOCSCTTY) β†’ close(master) β†’ open pty2 + TIOCSCTTY (orphans tp1) β†’ exit (session kfreed); parent closes the orphaned slave β†’ panic. hold mode + kern.ttys decode shows the orphaned TS_ZOMBIE tty with a non-NULL freed session pointer. uid0 route not completed (documented: M_SESSION LIFO recycle β†’ t_session aliases a chosen live session β†’ ctty hijack; ksignal sink gated by devfs invisibility of closed clone masters on the stock guest).

Dissociate the replaced tty inside TIOCSCTTY (token-guarded t_session/t_pgrp clear + pgrel) and reject TIOCSCTTY on D_MASTER fds (devfs_fo_ioctl / vn_ioctl). Full diff in findings/poc/DF-2687/fix.diff. Hunk 1 validated in-guest (nativekernel build + reboot): identical PoC + poison β†’ no panic, kern.ttys shows the orphan fully dissociated.

Timeline

  • 2026-08-30 Discovered during pass-2 audit of tty.c (GLM 5.3); deterministic unpriv panic reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2687 Β· 15 files
FileTypeDescriptionSize
df2687.c β€” 7.3 KB view raw
forensic.c β€” 1.2 KB view raw
build.sh β€” 291 B view raw
run.sh β€” 452 B view raw
build.log β€” 753 B view raw
run.log β€” 1.2 KB view raw
run.fix.log β€” 402 B view raw
panic.txt β€” 630 B view raw
forensic.baseline.txt β€” 661 B view raw
forensic.fixed.txt β€” 349 B view raw
env.txt β€” 536 B view raw
fix.diff β€” 2.3 KB view raw
FIXNOTES.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 6.4 KB ↓ raw
verdict.json β€” 5.0 KB view raw
VERDICT.md
↓ download raw

DF-2687 VERDICT β€” REPRODUCED (deterministic unprivileged kernel UAF, panic)

Bottom line

An unprivileged local user can leave a struct tty's t_session pointer dangling into freed struct session memory and then force the kernel to dereference it, with full control of the trigger timing. Demonstrated as a deterministic kernel panic (lwkt_gettoken GP-fault on 0xdeadc0dedeadc0de after enabling the INVARIANTS free-poison debug.use_weird_array) and as a raw dangling kernel heap pointer in the kern.ttys sysctl dump. A one-hunk fix in sys/kern/tty.c (dissociate the replaced tty in TIOCSCTTY) was built, installed, and verified to eliminate both the panic and the dangling pointer.

The bug, line by line

  1. ttioctl() TIOCSCTTY guard β€” sys/kern/tty.c:1183-1185: ((p->p_session->s_ttyvp || tp->t_session) && (tp->t_session != p->p_session)) β€” the only thing preventing a session from re-assigning its controlling tty is a non-NULL s_ttyvp (or tp->t_session equal to ours).
  2. The vnode layer records the ioctl fd's vnode as s_ttyvp (sys/vfs/devfs/devfs_vnops.c:1598-1611, sys/kern/vfs_vnops.c:1030-1052) with no validation that the fd is a terminal slave. A pty master fd is accepted.
  3. Last close of that master vnode runs devfs' controlling-tty half-close s_ttyvp = NULL (sys/vfs/devfs/devfs_vnops.c:1153-1156) and, because the ptm device's last close is ptcclose(), ttyclose() is never called (sys/kern/tty_pty.c:636-689 has no ttyclose), so tp->t_session survives while s_ttyvp is NULL.
  4. The session leader now passes the guard in (1) and TIOCSCTTYs a second tty: sys/kern/tty.c:1190-1203 β€” ttyhold(tp2), tp2->t_session = sp, otp = s_ttyp; s_ttyp = tp2; ttyunhold(otp). otp->t_session is never cleared β€” unlike every other s_ttyp transition, which is paired with ttyclearsession() (tty.c:279-316).
  5. Leader exits: sess_rele() repairs only sess->s_ttyp->t_session (sys/kern/kern_proc.c:930-943) and kfree()s the session (kern_proc.c:947). The orphaned tty's t_session now dangles.
  6. Dereference sites (unprivileged reachability on the stock guest): * final slave close → ptsclose → ttyclose (tty.c:261) → ttyclearsession: sp = tp->t_session (tty.c:294), prg = sp->s_prg (tty.c:295), lwkt_gettoken(&prg->proc_token) (tty.c:296) — deterministic, this is the panic we hit. * master carrier loss → ptcclose → ttymodem: tp->t_session->s_leader + ksignal() (tty.c:1623-1624) — reachable on configurations where the master can be re-opened (e.g. a second devfs mount, jails); not reachable on the stock guest because unix98 master nodes are devfs-invisible after device close. * TIOCGSID/isctty() (tty.c:1009-1015, sys/sys/tty.h:216-217) — safe NULL-wise (compares against the caller's live session) but becomes a session-aliasing oracle when the freed chunk is recycled into a new session: the orphaned tty starts acting as the new session's controlling terminal (ctty hijack, cross-session TIOCSPGRP / TIOCSWINSZ→pgsignal reach).

Reproduction evidence

  • panic.txt β€” Fatal trap 9, Stopped at lwkt_gettoken+0x64: movq (%r12),%rax, current process = the unprivileged PoC (pid 2179), guest wedged at db>.
  • forensic.baseline.txt β€” kern.ttys dump during PoC hold mode: orphaned TS_ZOMBIE pty (state=01100020, refs=0) with session=0xfffff80116d4e970 while its session is already freed (leader reaped) β€” the dangling pointer itself.
  • run.log β€” full run transcript (build + 40 pre-fix rounds + poison run leading to the panic).
  • Trigger chain: df2687.c steps (1)-(6), all unprivileged: open ptmx/pts β†’ fork L β†’ L: setsid, TIOCSCTTY(master fd), close master, TIOCSCTTY(pts2), exit β†’ parent closes orphaned slave β†’ UAF.

Determinism: every code path is sequenced by the program; the only heap dependency is what occupies the freed 104-byte chunk, which the debug.use_weird_array poison fixes at 0xdeadc0de (a kernel INVARIANTS facility provided precisely for exposing use-after-frees). With the flag off the dereference still occurs β€” it reads stale freed-memory contents.

Exploit chain (assessed, not completed to uid=0)

The panic is proof of the primitive. Toward escalation on this bug: groom M_SESSION chunks (setsid churn is same-type/same-size, LIFO per CPU) so the freed session is recycled into an attacker-influenced session (verified conceptually; the TIOCGSID alias oracle needs P_CONTROLT which a candidate can set via its own TIOCSCTTY). s_leader then points at a chosen live process β†’ ttymodem() delivers SIGHUP to it; conversely, in allocator states where the chunk holds non-session data, ksignal() is called on a raw heap-content pointer. DF's slab allocator is per-CPU, per-size-class and shared across malloc types, so cross-type grooming of the 128-byte class is possible given CPU affinity. Full uid=0 was NOT developed: on the stock guest the ttymodem sink is gated by devfs node invisibility of closed clone masters, and ttyclearsession's s_prg read is the only deterministic deref. The impact demonstrated here is therefore: reliable local DoS (panic) + permanent kernel state corruption (dangling pointer, leaked pgrp) + ctty hijack of recycled sessions.

Fix validation

  • fix.diff hunk 1 (tty.c) applied alone inside the guest (patch -p1), make -j6 nativekernel KERNCONF=X86_64_GENERIC (BUILD_RC=0, 357 s), make installkernel, reboot into DragonFly 6.5-DEVELOPMENT #1: Sun Aug 30 20:17:44 UTC 2026.
  • Baseline (stock #0 kernel, same PoC, debug.use_weird_array=1): Fatal trap 9 panic β€” reproduced.
  • Patched (#1 kernel): /tmp/df2687 3 β†’ all rounds complete, POC_RC=0, guest up β€” no panic.
  • Patched forensic (hold mode): state=01100020 dev=0x2100 pgrp=0x0 session=0x0 β€” the orphaned tty is now fully dissociated (both the dangling t_session and the leaked t_pgrp are gone).

Environmental notes

  • Guest: DragonFly 6.5-DEVELOPMENT, stock INVARIANTS kernel #0 (Thu Jul 2 06:02:54 UTC 2026), x86_64, 6 vCPUs, single devfs mount, no vfs.usermount.
  • unix98 ptys only (/dev/ptmx clone); no legacy /dev/pty?? devices.
  • debug.use_weird_array is 0 by default; enabling it does not change control flow of the bug β€” it poisons freed slab memory (first 64 bytes, 0xdeadc0de) so the UAF read faults instead of silently consuming stale data.
FIXNOTES.md
↓ download raw

Fix for DF-2687: orphaned tp->t_session UAF via TIOCSCTTY reassignment

Root cause (two cooperating defects)

  1. sys/kern/tty.c ttioctl() TIOCSCTTY moves the session's s_ttyp to the new tty and drops the old tty's hold (ttyunhold) but never clears the old tty's t_session / t_pgrp. Every other s_ttyp transition is paired with a full dissociation (ttyclearsession); this one is not.

  2. The vnode layer (sys/kern/vfs_vnops.c / sys/vfs/devfs/devfs_vnops.c) records the ioctl fd's vnode as s_ttyvp without validating that it is a terminal slave. Issuing TIOCSCTTY on a pty MASTER fd therefore makes the master vnode the session's ctty vnode; the master's last close then clears s_ttyvp (devfs_spec_close controlling-tty half-close) without running ttyclose() (ptcclose never calls it), leaving the session reassignable while the first tty's t_session still points at it.

After the session is freed (sess_rele only repairs sess->s_ttyp->t_session), the first tty's t_session dangles into freed M_SESSION memory and is dereferenced by ttyclearsession() (tp->t_session->s_prg -> lwkt_gettoken) on the final slave close and by ttymodem() (tp->t_session->s_leader -> ksignal) on master carrier loss.

Patch

  • tty.c: dissociate the replaced tty inside TIOCSCTTY (clear t_session and t_pgrp under its token, release the pgrp reference) before ttyunhold().
  • devfs_vnops.c / vfs_vnops.c: reject TIOCSCTTY on D_MASTER devices.
--- a/sys/kern/tty.c
+++ b/sys/kern/tty.c
@@ -1193,6 +1193,22 @@ case TIOCSCTTY:          /* become controlling tty */
        ttyhold(tp);
        tp->t_session = p->p_session;
        opgrp = tp->t_pgrp;
        pgref(p->p_pgrp);
        tp->t_pgrp = p->p_pgrp;
        otp = p->p_session->s_ttyp;
        p->p_session->s_ttyp = tp;
        p->p_flags |= P_CONTROLT;
        if (otp) {
+           /*
+            * Fully dissociate the tty we are replacing,
+            * otherwise its t_session pointer is left
+            * dangling into the (possibly soon-freed)
+            * session [DF-2687].
+            */
+           if (otp != tp) {
+               lwkt_gettoken(&otp->t_token);
+               if (otp->t_session == p->p_session) {
+                   otp->t_session = NULL;
+                   opgrp = otp->t_pgrp;
+                   otp->t_pgrp = NULL;
+                   if (opgrp) {
+                       pgrel(opgrp);
+                       opgrp = NULL;
+                   }
+               }
+               lwkt_reltoken(&otp->t_token);
+           }
            ttyunhold(otp);
        }
        if (opgrp) {

See fix.diff in this directory for the git-apply-able version including the devfs/vnops master-fd rejection.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Kernel rebuilt with fix.diff hunk 1 (sys/kern/tty.c TIOCSCTTY dissociation) only; exact same PoC with debug.use_weird_array=1 completes all rounds with no panic; kern.ttys forensic shows the orphaned tty fully dissociated (session=0x0, pgrp=0x0) where the stock kernel showed the dangling session pointer and panicked.

run.fix.log; forensic.fixed.txt; build.log (nativekernel BUILD_RC=0, installkernel INSTALL_RC=0)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Aug 30 20:17:44 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

unprivileged setsid+ptmx -> TIOCSCTTY(master fd) -> close(master) clears s_ttyvp w/o ttyclose -> TIOCSCTTY(pts2) orphans tp1 (t_session kept, s_ttyp moved, ttyunhold) -> leader exit kfrees session -> tp1->t_session dangles -> close(orphan slave) -> ttyclose->ttyclearsession->s_prg->lwkt_gettoken on freed memory -> Fatal trap 9 panic; escalation ceiling (not completed): M_SESSION chunk recycle makes tp1->t_session alias a chosen live session (ctty hijack, cross-session TIOCSPGRP/TIOCSWINSZ pgsignal, ttymodem ksignal(s_leader) where master reopen is possible)

Evidence (decisive lines)

["panic.txt: 'Fatal trap 9 ... Stopped at lwkt_gettoken+0x64: movq (%r12),%rax' with current process = unprivileged df2687", 'forensic.baseline.txt: orphaned tty state=01100020 refs=0 session=0xfffff80116d4e970 (non-NULL dangling) in kern.ttys while the session is freed', 'forensic.fixed.txt: same orphan state after fix -> pgrp=0x0 session=0x0', 'run.log / run.fix.log: baseline panic vs patched 3 clean rounds with debug.use_weird_array=1', 'build.log: nativekernel BUILD_RC=0 (357s) + installkernel for the fix validation']

PoC changes

Replaced TIOCGPTN (EAGAIN-gated before slave open on DF) with fstat(st_rdev) minor unit discovery; fixed pipe end mixup; kept slave fd open in parent across leader exit (prevents VOP_CLOSE from running ttyclose early); added hold-mode for kern.ttys forensics; final trigger is the slave close (ttyclearsession sink) after enabling debug.use_weird_array poison; session-alias candidate swarm retained for the TIOCGSID oracle.

Verified recommended fix

TIOCSCTTY: fully dissociate the replaced tty (clear t_session/t_pgrp under its token, pgrel) before ttyunhold(); reject TIOCSCTTY on D_MASTER fds in devfs_fo_ioctl/vn_ioctl before the device ioctl runs (fix.diff)

Verdict

Deterministic unprivileged kernel use-after-free reproduced on the stock INVARIANTS guest: TIOCSCTTY on a pty MASTER fd makes the master vnode the session's ctty vnode; its last close clears s_ttyvp (devfs half-close) without ttyclose(), so tp->t_session survives; the leader then re-TIOCSCTTYs a second tty and exits; sess_rele() frees the session leaving the first tty's t_session dangling (proven non-NULL in kern.ttys for a TS_ZOMBIE orphan with refs=0); closing the orphaned slave dereferences sp->s_prg in ttyclearsession() and lwkt_gettoken() GP-faults on the INVARIANTS poison 0xdeadc0de (Fatal trap 9, lwkt_gettoken+0x64, pid of unprivileged PoC). Without the poison flag the same deref silently consumes freed-memory contents (ksignal-through-freed-s_leader reachable in configurations where the master can be re-opened). fix.diff hunk 1 (dissociate replaced tty in TIOCSCTTY) built, installed and verified: no panic and kern.ttys shows session=0x0/pgrp=0x0 for the orphan.