# DF-3277 — VERDICT

**REPRODUCED — kernel panic (KASSERT), with a documented escalation
path to use-after-free / double-free on non-INVARIANTS builds.**

## Environment

Same guest as DF-3276 (DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC,
6 CPUs; file byte-identical to DragonFlyBSD master).

## Root cause (code)

- `bridge_delete_span()` if_bridge.c:1155:
  `TAILQ_REMOVE(&sc->sc_iflists[mycpuid], bif, bif_next);` — the bif is
  on `sc->sc_spanlist` (inserted at if_bridge.c:1832).  Removing the
  last span element runs `head->tqh_last = elm->tqe_prev` with the
  wrong head: `sc_iflists[0].tqh_last := &sc->sc_spanlist.tqh_first`.
- `bridge_select_unicast()` if_bridge.c:2135-2143: with LINK2 and a
  bonding set (equal-MAC members, equal priority, both forwarding),
  every `bif_bond_weight` packets it does
  `TAILQ_REMOVE(...); TAILQ_INSERT_TAIL(&sc->sc_iflists[mycpuid],
  alt_bif, bif_next);` — the INSERT_TAIL writes
  `sc_spanlist.tqh_first = alt_bif` through the stale pointer.

## Reproduction narrative (3 panics; full chain captured on the 3rd)

Iterations (all on the single-tenant QEMU guest):

1. First runs stalled: STP ports never reached FORWARDING with no
   peers — root-caused to `bstp_enable_port()` parking enabled ports
   in BLOCKING without a state selection; fixed in the PoC by opening
   the tap fds (media active) *before* bridge-up so
   `bstp_initialization()`'s final `bstp_port_state_selection()` drives
   them LISTENING→LEARNING→FORWARDING.
2. Learn/lookup frames initially ETHERTYPE_IP with zero payload were
   freed by `ip_lengthcheck` (sys/netinet/ip_demux.c:163, ip_hl=0)
   before ever reaching `bridge_input`.  Switched to ETHERTYPE_ARP —
   NETISR_ARP has no hashfn so hash==0 and all processing happens on
   cpu0, matching the corrupted `sc_iflists[0]`.
3. Full chain run (serial-console capture, run.log):

```
    t= 2 tap0: state=3 flags=0x17 | tap1: state=3 flags=0x37   # FORWARDING
[+] spanport tap3 added+deleted (list head corrupted)
[+] list order before bonding traffic:  [member order: tap2 tap1 tap0]
[+] after bcast learn frame:  [rtable: 02:00:00:00:00:55->tap0]  # learning works
[+] list order after bonding traffic:   [member order: tap2 tap0 tap1]  # INSERT_TAIL fired
[+] rtable after:  [02:...:66->tap2 02:...:55->tap0]
panic: bridge_delete_span: not a span interface
bridge_delete_span() at bridge_delete_span+0x87
bridge_ioctl_delspan() at bridge_ioctl_delspan+0x58
bridge_control_dispatch() at bridge_control_dispatch+0x64
netmsg_service_loop() at netmsg_service_loop+0xe9
```

The `delspan("tap1")` probe found tap1 — a member whose
`if_bridge == sc` — on the span list; `bridge_delete_span`'s KASSERT
(if_bridge.c:1152) fired.  Three independent runs panicked with the
identical signature.

## Impact / escalation

- On INVARIANTS kernels (like this guest): deterministic panic.
- On stock kernels the same chain silently **unlinks the member bif
  from the per-cpu member list and kfrees it while the interface is
  still a bridge member** (`if_bridge` still points at the softc; the
  other cpus' member lists still hold their bifs).  Subsequent
  teardown hits `KKASSERT(bif != NULL && bif->bif_onlist)` in
  `bridge_del_bif_handler` (KKASSERT is compiled unconditionally) or
  kfrees memory still referenced by the span list — a
  use-after-free/double-free of M_DEVBUF objects.  With
  attacker-shaped heap grooming (the freed bif is 40-ish bytes in the
  devbuf slab) this is a memory-corruption primitive; both the
  corruption writer (bonding round-robin) and the freeing path are
  driven by packet traffic once root has configured the topology.
- Configuration requires root (`BRDGADDS`/`BRDGDELS`/`BRDGSIFFLGS` are
  `BC_F_SUSER`-gated), hence Medium severity rather than High.

## Fix validation

`fix.diff` (one-line: unlink from `&sc->sc_spanlist`) applied to the
guest's /usr/src alongside the DF-3276 fix; kernel rebuilt and
rebooted; the identical PoC then ran to completion: bonding
round-robin still reorders the member list, but the delspan("tap1")
probe correctly returns ENOENT and `ifconfig bridge0 destroy` completes
cleanly (run_fixed.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.
