# DF-4669 — VERDICT

**Status: reproduced. Impact: panic via slab-detected double free (deterministic
UAF, no race). Confidence: certain.**

## What was run

- Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026 (QEMU/KVM),
  fresh `vm.sh reset with-src`; setup `kldload snp` as root.
- Build: `cc -O1 -o /tmp/snp_reattach /tmp/snp_reattach.c -lutil` (as maxx).
- Run: `/tmp/snp_reattach` as root (SNPSTTY needs /dev/snpN, 0600 root:wheel).
  Output up to the crash was lost with the ssh connection; the serial console
  captured the panic.

## Decisive evidence (panic.txt)

```
panic: memory chunk 0xfffff80116d96000 is already free!
cpuid = 3
chunk_mark_free() at chunk_mark_free+0xae
_kfree() at _kfree+0x262
snp_down() at snp_down+0xa0
snplclose() at snplclose+0x2b
ptsclose() at ptsclose+0x71
Debugger("panic")
```

`snp_down+0xa0` is `kfree(snp->snp_buf, M_SNP)` at snp.c:518 — executed with
`snp` = the `struct snoop` freed by snpclose, whose stale `snp_buf` (offset 56)
still points at the 4096-byte buffer snpclose already freed at snp.c:496.

## Why it reproduces (path:line trace)

1. SNPSTTY #1 (snp.c:540-580): tty A gets `t_sc=snp`, `TS_SNOOP`,
   `t_line=snooplinedisc`; `snp_target=snp_tty=A`; `snp_olddisc=A's old disc`.
2. SNPSTTY #2: the un-poison block `if (snp->snp_target == NULL)` (snp.c:560)
   is DEAD CODE on re-attach — `snp_target`/`snp_tty` are only ever set/cleared
   together (snp.c:434-435, 473-474, 570-571) — so tty A keeps all three poison
   values while the snp switches to tty B and `snp_olddisc` is overwritten with
   B's old discipline (A's becomes unrecoverable).
3. close(snp fd): `snpclose` (snp.c:484-508) frees `snp_buf`, clears
   SNOOP_OPEN, `snp_detach` detaches *tty B only* (snp_tty==B) and frees the
   snoop struct (snp.c:478-479). Tty A still holds dangling `t_sc`.
4. close(tty A): `ptsclose` → `snplclose` (snp.c:129) reads freed
   `tp->t_sc` → `snp_down(freed)` (snp.c:514): `snp_blen` still 0 (set by
   snpclose:494) != 4096 → `kfree(stale snp_buf)` → double free caught by
   `chunk_mark_free` → panic. (Had it not been caught, `snp_detach` would also
   `kfree(snp)` a second time — snp.c:478-479 with SNOOP_OPEN already clear.)

No race required — purely sequential ioctls + closes.

## Primitive characterization (per contract: push past the panic)

The primitive is `kfree(*(void **)(freed_chunk + 56))` — an attacker-influenced
free of a pointer read from freed M_SNP memory — plus a second free of the
freed 80-byte struct itself. DFly's slab allocator selects zones by SIZE CLASS
ONLY (`zoneindex(&size)` / `slgd->ZoneAry[zi]`, sys/kern/kern_slaballoc.c:967-971);
the malloc type only does accounting. So a reallocated-in-between 4096 chunk
becomes a live object of ANY subsystem in that size class, and the second
`snp_down` kfree frees a LIVE chunk → cross-type aliasing (classic double-free
→ two owners → type confusion). Reclaim of the 80-byte struct is equally
cross-type (any 96-class allocation).

**Why uid=0 was not chained end-to-end here:** the trigger chain requires
opening `/dev/snp` (0600 root:wheel) and issuing SNPSTTY — an unprivileged
user cannot reach the primitive at all (device permissions, snp.c:705/723),
so this is not an unpriv→root bug; it is a privileged-user→kernel-heap
corruption with cross-type reclaim potential. Given the wheel-gated trigger,
the escalation value is limited, and the deterministic double-free panic (plus
the reclaim analysis above) is the honest impact ceiling: kernel memory
corruption reachable from wheel.

## Fix validation

`fix.diff` replaces the dead guard with a real un-poison (restore old tty's
`t_sc`/`TS_SNOOP`/`t_line` under `tpo->t_token` before re-attaching). Applied
to the guest /usr/src, kernel rebuilt (`make nativekernel`), both PoCs re-run —
see verdict.json `fix_*` fields and run.2.log.

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