DF-4669 / verdict.json
{ "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" } |