DF-2687 / verdict.json
{ "finding_id": "DF-2687", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "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.", "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": [ "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" ], "kernel_refs": [ "sys/kern/tty.c:1181-1204", "sys/kern/tty.c:279-316", "sys/kern/tty.c:294-296", "sys/kern/tty.c:1623-1624", "sys/kern/tty.c:246-266", "sys/vfs/devfs/devfs_vnops.c:1598-1611", "sys/vfs/devfs/devfs_vnops.c:1146-1156", "sys/kern/tty_pty.c:636-689", "sys/kern/kern_proc.c:896-948" ], "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.", "attempts": 8, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline, panic) / #1: Sun Aug 30 20:17:44 UTC 2026 (fix validation)", "runtime_sec": 4200, "guest_dirty": 1, "build_cmd": "cc -O2 -Wall -o /tmp/df2687 /home/pocs/df2687.c ; cc -O1 -D_KERNEL_STRUCTURES -o /tmp/forensic /home/pocs/forensic.c", "run_cmd": "sysctl debug.use_weird_array=1 && su -m pocs -c '/tmp/df2687 1' (panic on stock; clean on patched)", "code_hash": "942cefd9f3959ad541857208350b068c4976c98a5a1efd161f4c084eefe5bc69", "notes": "debug.use_weird_array (INVARIANTS, default 0) only poisons freed slab chunks so the unconditional UAF read faults observably; it does not create the bug. unix98 master cdevs are devfs-invisible after device close on the stock guest, which gates the ttymodem/ksignal sink here but not in configs with a second devfs mount (jails). Same-type LIFO recycle of the 104-byte M_SESSION chunk yields session aliasing (ctty hijack). Also leaks a pgrp reference per round (t_pgrp of the orphan). Guest reset to clean-source snapshot after validation.", "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)", "fix_status": "fixed", "fix_kernel_uname": "DragonFly 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", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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.", "fix_evidence": "run.fix.log; forensic.fixed.txt; build.log (nativekernel BUILD_RC=0, installkernel INSTALL_RC=0)" } |