{
  "finding_id": "DF-4669",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "Deterministic (no-race) use-after-free / double free in the M_SNP heap reproduced as root/wheel: SNPSTTYs re-attach cleanup is dead code (guard snp_target==NULL is never true on re-attach because snp_target/snp_tty are only set/cleared together), so the old target tty keeps t_sc=snp + TS_SNOOP + t_line=snooplinedisc; closing the snp fd frees struct snoop; closing the old tty runs snplclose -> snp_down(freed snp) -> kfree(stale snp_buf). Slab allocator catches it: panic: memory chunk ... is already free! with stack chunk_mark_free <- _kfree <- snp_down <- snplclose <- ptsclose. Patched kernel: full poison chain runs clean, exit=0, no panic.",
  "exploit_chain": "wheel-gated: open /dev/snpN -> SNPSTTY(ttyA) -> SNPSTTY(ttyB) [A poisoned] -> close(snp fd) [struct snoop + snp_buf freed, A dangling] -> close(ttyA) -> snplclose -> snp_down(freed): kfree(*(freed+56)) double-free of the 4096-byte chunk + (if reached) second kfree of the 80-byte struct. DFly slab zones are selected by size class only (kern_slaballoc.c:967 zoneindex/ZoneAry), NOT by malloc type, so reallocating the freed chunks in between turns this into a free-of-live-chunk with cross-type aliasing (two owners of one chunk). Not reachable from a non-wheel user: /dev/snp is 0600 root:wheel, so unpriv->root is out; honest ceiling is privileged-user->kernel heap corruption / reliable panic.",
  "evidence": "panic.txt: panic: memory chunk 0xfffff80116d96000 is already free! + stack chunk_mark_free <- _kfree <- snp_down+0xa0 <- snplclose+0x2b <- ptsclose+0x71 (= kfree at snp.c:518 operating on the freed structs stale snp_buf). run.2.log: patched kernel survives the identical sequence.",
  "kernel_refs": [
    "sys/dev/misc/snp/snp.c:560",
    "sys/dev/misc/snp/snp.c:563",
    "sys/dev/misc/snp/snp.c:484",
    "sys/dev/misc/snp/snp.c:496",
    "sys/dev/misc/snp/snp.c:517",
    "sys/dev/misc/snp/snp.c:478",
    "sys/kern/kern_slaballoc.c:967"
  ],
  "poc_changes": "Seed sketch fixed: <libutil.h> instead of <pty.h>, link -lutil; SNPSTTY takes the ttys st_rdev (dev_t u32) directly; keep ttyB open so the re-attach actually poisons A while snp_tty points at B.",
  "attempts": 1,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
  "runtime_sec": 300,
  "guest_dirty": 0,
  "build_cmd": "cc -O1 -o /tmp/snp_reattach /tmp/snp_reattach.c -lutil",
  "run_cmd": "vm.sh run_root kldload snp; /tmp/snp_reattach",
  "code_hash": "e2950c5bf014e8dee80d44f36152ec1e986d633ba15ca9d4a85f8847e08290df",
  "notes": "Stronger than DF-1804 (which needs a concurrent-race window): this is sequential ioctls+close, 100% deterministic. The old block also wrote tpo->t_state without tpos token; the fix takes tpo->t_token and restores t_sc/TS_SNOOP/t_line (to remembered snp_olddisc) before the new attach overwrites it.",
  "recommended_fix": "SNPSTTY: replace the dead if (snp->snp_target == NULL) block with a real un-poison of snp->snp_tty under its token (clear t_sc/TS_SNOOP, restore t_line from snp_olddisc) before attaching to the new tty.",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Same fix.diff (both hunks) applied, kernel rebuilt and installed; /tmp/snp_reattach as root on patched kernel: both SNPSTTYs succeed, snp fd close succeeds, close(ttyA) succeeds, exit=0, guest up, console clean. Baseline double-free panic in panic.txt.",
  "fix_evidence": "run.2.log (patched: no panic observed, exit=0); panic.txt (baseline slab double-free); fix.diff; build.log"
}