β¬’ 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 Β· 24 files
FileTypeDescriptionSize
df2687.c β€” 7.3 KB view raw
df2687x.c β€” 8.3 KB view raw
df2687race.c β€” 5.4 KB view raw
forensic.c β€” 1.2 KB view raw
build.sh β€” 185 B view raw
run.sh β€” 357 B view raw
pin.sh β€” 511 B view raw
race.sh β€” 705 B view raw
build.log β€” 753 B view raw
run.log β€” 1.2 KB view raw
run.fix.log β€” 402 B view raw
run.pin.log β€” 1.7 KB view raw
run.alias.log β€” 1018 B view raw
run.race.log β€” 3.8 KB view raw
run.poison1.log β€” 1008 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
env.round2.txt β€” 987 B view raw
fix.diff β€” 2.3 KB view raw
FIXNOTES.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 13.0 KB ↓ raw
verdict.json β€” 7.7 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)

[Round-2 correction, 2026-10-16] The recycling scenarios assessed below ("next setsid() reuses the freed chunk", session aliasing, TIOCGSID oracle) were refuted by measurement β€” see R1/R2 above: the chunk is pinned until the orphan's own close and the dangling read happens ~10 instructions after the same-syscall free. They are kept here only as the historical round-1 assessment.

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.

Round 2 β€” weaponization attempt (2026-10-16, exploit-development mode)

Goal: zero-precondition unprivileged uid=0 (or equivalent) from this bug on the stock guest. Terminal result: not achievable β€” blocked structurally, not by luck. What was done and proven:

R1. The pin invariant (corrects round 1's lifecycle model)

Round 1 asserted the session is "already freed (leader reaped)" while the orphan slave is held, implying an unbounded reclaim window. That is wrong. The orphan's leaked t_pgrp reference keeps the session's pgrp alive, and a live pgrp's session reference (s_count, sys/kern/kern_proc.c:783/797) pins the session. Measured on the guest (run.pin.log):

  • vmstat -m M_SESSION in-use: BEFORE=19, MIDHOLD=27 (+8 while 8 orphan slaves are held open), AFTER=19 β€” the chunks free exactly at the orphan's own close(), never before.

Consequence: the free and the dangling read happen inside the same ttyclearsession() invocation: pgrel(leaked pgrp) β†’ sess_rele() β†’ kfree(session) (kern_proc.c:947) followed ~10 instructions later by sp = tp->t_session; prg = sp->s_prg (tty.c:294-295). The reclaim window is that same-thread, same-syscall sliver β€” there are no sleep points after kfree(sess) (only lwkt_reltoken, kfree(pgrp), two returns).

R2. The recycled-session-hijack route is closed

run.alias.log: 0/256 setsid candidates ever aliased the orphan (TIOCGSID oracle never fired) β€” impossible under the pin model, and consistent with round 1's own 40-round transcript (no alias ever observed). The "ctty hijack of recycled sessions" escalation assessed in round 1 cannot occur on stock configuration.

R3. The reclaim race is unwinnable by userland

An unprivileged controlled-content reclaim primitive for the 104-byte session zone exists and was weaponized (df2687race.c): the sysctl KERN_PROC_ARGS self-write performs kmalloc(8+96=104, M_PARGS) + SYSCTL_IN(96 controlled binary bytes) + kfree (kern_proc.c:1962-1971) β€” same per-size slab zone as struct session (zones are shared across malloc types, kern_slaballoc.c zoneindex), fully controlled, repeatable at full speed. Detection was non-destructive: spray s_prg = VA of a private userland fake lwkt token (no SMAP on this guest) with t_count=TOK_EXCLREQ(2); a kernel token dance on it leaves the persistent trace t_count 2β†’0.

Result (run.race.log): 8750 window attempts, 0 hits. Structural reasons, not variance:

  1. No sleep/block points exist in the window (above).
  2. DragonFly LWKT does not switch a CPU-bound user thread to another user thread mid-syscall; switches happen at block/yield/boundary points. Only interrupt context can interleave, and nothing interrupt-side allocates from the 104-byte kmalloc zone.
  3. A chunk freed to a CPU's zone array can only be handed back out by a subsequent allocation on that same CPU (or a remote drain), neither of which userland can cause inside the window.

So on a default-config kernel the dangling deref always consumes the stale-but-valid pre-free contents (s_prg = the real procglob pointer), the token dance is a no-op, and close() returns cleanly. Cumulative empirical support: 40 (round 1) + 3 + 8 + 8750 closes, zero panics without the INVARIANTS poison. The round-1 deterministic panic requires debug.use_weird_array=1 β€” a root-set, INVARIANTS-only debugging aid (the poison code is #ifdef INVARIANTS, kern_slaballoc.c:1433-1438; the current reset snapshot's kernel is non-INVARIANTS and the sysctl is inert there β€” run.poison1.log).

R4. Even with full chunk control the sink ceiling is zero

The only deterministic consumer of the dangling pointer is lwkt_gettoken(&sp->s_prg->proc_token) / lwkt_reltoken() on ONE fully-controlled 64-bit value (s_prg; &prg->proc_token == prg since proc_token is the first member of struct procglob). That dance is by construction reversible (net-zero RMW at one address): exclusive-acquire CAS (count & ~EXCLREQ)|EXCLUSIVE then release atomic_clear_long(TOK_EXCLUSIVE) + t_ref = NULL (lwkt_token.c:262-296, 439-459); the contended path merely ORs TOK_EXCLREQ and blocks the closer itself (self-DoS). The powerful sinks are all unreachable:

  • ttymodem()'s ksignal(sp->s_leader, SIGHUP) (tty.c:1623-1624) needs a master carrier-loss after the free; ordering makes this impossible (the master close that orphans the tty precedes the session free), and master re-open is fenced on stock single-devfs mounts (no /dev/ptm on this guest; DF-2769's control run was FENCED for the same reason).
  • sess_rele() on a controlled chunk (LIST_REMOVE(sess, s_list) = a controlled-address write; ttyunhold(sess->s_ttyp) = a tp->t_unhold function-pointer call, tty.c:1580-1589 β€” would be an instant ring-0 with SMEP off) and pgrel()'s write through pg_session->s_ttyp (kern_proc.c:683-685) require the session/pgrp to free while still referenced. The pin invariant (R1) plus the fact that a live process's own pgrp always pins its session (a live process's p->p_session can never dangle) close every ordering that would reach them. Each was traced exhaustively (multi-orphan chains, pgrp swaps via TIOCSPGRP, alias-repair TIOCSCTTY, concurrent closers racing pgrel, the subr_prf.c tprintf transient sess_hold, exit1 revoke paths).

Bottom line of round 2

On the stock guest, DF-2687 is a genuine lifetime bug (orphaned t_session/t_refs=0 tty, leaked pgrp reference) whose dangling dereference is structurally guaranteed to consume stale-valid contents. Zero-precondition unprivileged impact: none observable (silent). Root-assisted INVARIANTS demonstration: deterministic panic (round 1). uid=0: not reachable from this bug on stock configuration; the strongest theoretical primitive (arbitrary-address atomic RMW via a forged s_prg) is both unreachable (R3) and net-zero (R4). The fix from round 1 remains correct and validated.

Round-1 fix validation (retained)

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

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.

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

attempted: unpriv orphan chain -> hold -> KERN_PROC_ARGS 104-byte controlled reclaim churn (df2687race.c) -> close(orphan) racing the ~30ns same-syscall window between kfree(session) and the sp->s_prg read -> forged s_prg = userland fake lwkt token (no SMAP) -> persistent trace t_count 2->0. Result: 0/8750 hits; window is structurally uninterleavable by userland (no mid-syscall user-thread switches, no interrupt-side 104-zone allocations, no sleep points post-free). uid0 chain not reachable: sink inventory for the dangling t_session on stock config = {net-zero token dance} only; ttymodem ksignal gated; sess_rele/pgrel controlled-chunk sinks impossible while pin invariants hold

Evidence (decisive lines)

['run.pin.log: BEFORE=19 MIDHOLD=27 AFTER=19 M_SESSION in-use -> session chunks pinned while orphan slaves held, freed exactly at close', 'run.alias.log: 0/256 setsid candidates aliased the orphan -> recycled-session hijack impossible', 'run.race.log: df2687race 3 8 60 -> ~8750 window attempts, 0 fake-token trace hits, plus structural analysis of why', 'run.poison1.log: poison sysctl inert on non-INVARIANTS snapshot #1; bug presence confirmed behaviorally via pin experiment', 'panic.txt / run.log (round 1): deterministic INVARIANTS-poison panic at lwkt_gettoken+0x64, unprivileged trigger', 'df2687race.c / df2687x.c: round-2 sources (race weaponizer with fake-token trace detector; pin/alias probes)']

PoC changes

Round 2 added: df2687x.c (pin/alias/spray probes) and df2687race.c (weaponization race: 3 closer threads x orphan chains + 8 sprayer threads x KERN_PROC_ARGS 104-byte controlled transient alloc churn; benign persistent-trace detector via userland fake lwkt token with t_count=TOK_EXCLREQ transitioning 2->0 on consumption; no crash needed to detect a hit). Round-1 trigger unchanged.

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

Round 2 (weaponization mode) terminal: uid=0 is NOT achievable from DF-2687 on the stock single-devfs guest, blocked structurally rather than probabilistically. (1) Pin invariant measured: the orphan's leaked t_pgrp pgref pins the session's pgrp, which pins the session (s_count) - vmstat -m shows M_SESSION in-use +8 while 8 orphan slaves are held and back to baseline only at their closes; the session frees INSIDE the orphan's own ttyclearsession (pgrel->sess_rele->kfree) ~10 instructions before the same function reads tp->t_session->s_prg. There is no persistent reclaim window and no sleep point after the free, so the dangling deref always consumes stale-but-valid contents (s_prg = real procglob) and close() returns cleanly - 3+8+8750 more closes in round 2 with zero panics, matching round 1's 40 surviving rounds. The round-1 deterministic panic requires the root-set INVARIANTS-only debug.use_weird_array poison (kern_slaballoc.c:1433-1438); zero-precondition unprivileged impact is silent. (2) The recycled-session-hijack route is closed: 0/256 setsid candidates ever aliased the orphan (TIOCGSID oracle never fired), consistent with the pin model and with round 1's own logs. (3) A real unprivileged controlled-content reclaim primitive was built (sysctl KERN_PROC_ARGS self-write = kmalloc(104)+96 controlled bytes+kfree, same slab zone as struct session) and raced against the window for 8750 attempts with a persistent non-destructive fake-token trace detector: 0 hits, explained by DFly LWKT not switching user threads mid-syscall and nothing interrupt-side allocating the 104-byte zone. (4) Even with full chunk control the sole deterministic consumer is the lwkt_gettoken/lwkt_reltoken pair on one controlled s_prg value - by construction net-zero (reversible CAS/clear; contended path only self-DoSes); the powerful sinks (ttymodem ksignal(s_leader), sess_rele LIST_REMOVE + ttyunhold(s_ttyp)->t_unhold function-pointer call, pgrel write through pg_session->s_ttyp) are each unreachable: ksignal is gated by master-reopen fencing (no /dev/ptm) and ordering, and the sess_rele/pgrel-on-controlled-chunk sinks require the session/pgrp to free while still referenced, which the pin invariant plus live-process pgrp pinning prohibit in every ordering traced. Round-1 panic and fix validation stand unchanged.