bridge_delete_span() TAILQ_REMOVEs span bif from the per-cpu MEMBER list head β list-tail corruption cross-links span and member lists, leading to KASSERT panic / member-bif UAF and double-free
Summary
bridge_delete_span() executes TAILQ_REMOVE(&sc->sc_iflists[mycpuid], bif, bif_next) (:1155) but span bifs are linked on sc_spanlist (inserted at :1832). Removing the last span entry runs head->tqh_last = elm->tqe_prev against the member-list head, leaving sc_iflists[0].tqh_last = &sc->sc_spanlist.tqh_first. The LINK2 bonding round-robin TAILQ_INSERT_TAIL (:2140) then writes a member bif into sc_spanlist.tqh_first - cross-linking the lists. REPRODUCED 3x end-to-end on stock INVARIANTS guest ('panic: bridge_delete_span: not a span interface' after bonding traffic rotated member order). On non-INVARIANTS kernels the same chain unlinks and kfrees a live member bif while if_bridge still points at the softc and other cpus still hold their bifs - UAF/double-free at teardown (M_DEVBUF slab corruption, timing attacker-influenced via traffic). Root configures (bonding set + span port); deleting the span port corrupts; bonding traffic cross-links; teardown frees live objects. Fix validated (one-line: TAILQ_REMOVE from sc_spanlist): probe returns ENOENT, destroy clean.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3277 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| bond_corrupt.c | β | 13.9 KB | view raw | |
| verdict.json | β | 4.1 KB | view raw | |
| env.txt | β | 743 B | view raw | |
| build.log | β | 341 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| run.sh | β | 153 B | view raw | |
| panic.txt | β | 2.9 KB | view raw | |
| run.log | β | 2.6 KB | view raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| run_fixed.log | β | 1.5 KB | view raw |
DF-3277 β bridge_delete_span() unlinks span bif from the WRONG list β member/span list cross-linkage β KASSERT panic / UAF / double-free
What
bridge_delete_span() executes
TAILQ_REMOVE(&sc->sc_iflists[mycpuid], bif, bif_next); /* if_bridge.c:1155 */
but span bifs are linked on sc->sc_spanlist
(TAILQ_INSERT_HEAD(&sc->sc_spanlist, bif, bif_next),
if_bridge.c:1832). When the removed span bif is the last element
of the span list (tqe_next == NULL), TAILQ_REMOVE executes
head->tqh_last = elm->tqe_prev against the per-cpu member list
head, so sc_iflists[0].tqh_last is clobbered to point at
&sc->sc_spanlist.tqh_first.
The next TAILQ_INSERT_TAIL on the member list β the LINK2 bonding
round-robin in bridge_select_unicast() (if_bridge.c:2135-2143) β
then executes *(sc_iflists[0].tqh_last) = alt_bif, i.e. it writes a
member bif pointer into sc_spanlist.tqh_first, cross-linking the
two lists. After that, span-list operations treat live member bifs as
span ports: bridge_delete_span()'s KASSERT fires (INVARIANTS), or on
stock kernels the member bif is unlinked from the member list and
freed while still a member β use-after-free / double-free at teardown
(bridge_del_bif_handler KKASSERTs bif != NULL, or kfree hits freed
memory).
Reproduce (guest, as root)
cd /root/poc/df-3277 cc -O -o bond_corrupt bond_corrupt.c ./bond_corrupt
The PoC (pinned to cpu0, all frames ETHERTYPE_ARP so the netisr hash keeps processing on cpu0):
- bridge0 with tap0/tap1 sharing one MAC (bonding set, STP on,
fwddelay 1s), tap2 normal member; bridge
link2+ up; tap fds opened before bridge-up so STP ports reach FORWARDING. spanport tap3+-spanport tap3β corruptssc_iflists[0].tqh_last.- Learn
02:00:00:00:00:55on tap0, then unicast to it from tap2 β bonding round-robinTAILQ_INSERT_TAILthrough the staletqh_lastβsc_spanlist.tqh_first = <tap1's member bif>. - Probe:
BRDGDELS("tap1")β finds the MEMBER on the span list.
Expected
[+] list order before bonding traffic: [member order: tap2 tap1 tap0] [+] after bcast learn frame: [rtable: 02:00:00:00:00:55->tap0] [+] list order after bonding traffic: [member order: tap2 tap0 tap1] panic: bridge_delete_span: not a span interface bridge_delete_span() at bridge_delete_span+0x87 bridge_ioctl_delspan() at bridge_ioctl_delspan+0x58
The member-order flip (tap1 moved to tail) is the INSERT_TAIL firing;
the probe panic proves tap1 β a member with if_bridge == sc β was
linked on sc_spanlist.
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 onsc->sc_spanlist(inserted at if_bridge.c:1832). Removing the last span element runshead->tqh_last = elm->tqe_prevwith 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), everybif_bond_weightpackets it doesTAILQ_REMOVE(...); TAILQ_INSERT_TAIL(&sc->sc_iflists[mycpuid], alt_bif, bif_next);β the INSERT_TAIL writessc_spanlist.tqh_first = alt_bifthrough the stale pointer.
Reproduction narrative (3 panics; full chain captured on the 3rd)
Iterations (all on the single-tenant QEMU guest):
- 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 sobstp_initialization()'s finalbstp_port_state_selection()drives them LISTENINGβLEARNINGβFORWARDING. - 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 reachingbridge_input. Switched to ETHERTYPE_ARP β NETISR_ARP has no hashfn so hash==0 and all processing happens on cpu0, matching the corruptedsc_iflists[0]. - 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_bridgestill points at the softc; the other cpus' member lists still hold their bifs). Subsequent teardown hitsKKASSERT(bif != NULL && bif->bif_onlist)inbridge_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/BRDGSIFFLGSareBC_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.
Fix verification
fixedRebuilt kernel (combined fix_all.diff): identical PoC runs to completion -- member learning and bonding round-robin still operate (on the correct list), the delspan('tap1') probe correctly returns ENOENT, ifconfig destroy completes cleanly, no panic, guest up.
findings/poc/DF-3277/run_fixed.log; findings/poc/DF-3277/fix.diff
Confirmed kernel references
Detail
Exploit chain
spanport add/del (root) -> sc_iflists[0].tqh_last corrupted -> bonding INSERT_TAIL (packet-driven, LINK2 + same-MAC members) -> sc_spanlist.tqh_first = member bif -> delspan/destroy walks member bif as span: KASSERT panic (INVARIANTS) or unlink+kfree of live member bif -> UAF/double-free (stock kernels). Memory-corruption primitive is root-config-gated, then traffic-driven.
Evidence (decisive lines)
['run.log: full serial-console chain -- states FORWARDING, member-order rotation, rtable learn, then the panic', 'panic.txt: second independent reproduction, identical stack (bridge_delete_span+0x87 <- bridge_ioctl_delspan+0x58)', 'run_fixed.log: patched kernel -- same PoC, bonding still reorders, delspan(tap1) probe returns ENOENT, destroy clean, guest up', 'fix.diff: one-line TAILQ_REMOVE head correction']
PoC changes
Written from scratch. Three PoC-environment pitfalls solved en route (documented so the pack reproduces first try): (1) tap fds must be opened BEFORE bridge-up so bstp_ifupdstatus sees IFM_ACTIVE and bstp_initialization drives ports to FORWARDING (bstp_enable_port parks enabled ports in BLOCKING without a state selection); (2) frames must be ETHERTYPE_ARP -- NETISR_ARP has no hashfn so processing stays on cpu0 (ETHERTYPE_IP frames with ip_hl=0 are freed by ip_lengthcheck before the bridge); (3) process pinned to cpu0 via sched_setaffinity (DragonFly has no cpuset(1)/sys/cpuset.h).
Verified recommended fix
bridge_delete_span: TAILQ_REMOVE(&sc->sc_spanlist, bif, bif_next) instead of &sc->sc_iflists[mycpuid]
Verdict
List-head confusion: bridge_delete_span() TAILQ_REMOVEs the span bif against &sc->sc_iflists[mycpuid] although span bifs live on sc_spanlist (if_bridge.c:1155 vs 1832). Removing the last span entry clobbers sc_iflists[0].tqh_last to &sc->sc_spanlist.tqh_first; the LINK2 bonding round-robin TAILQ_INSERT_TAIL (if_bridge.c:2140) then writes a member bif into sc_spanlist.tqh_first, cross-linking the lists. Reproduced three times end-to-end on the stock guest with a full instrumentation chain: same-MAC STP tap pair (FORWARDING), link2 bridge, spanport add+del, ARP-frame learn+forward pinned to cpu0 (member order visibly rotated by the round-robin: 'tap2 tap1 tap0' -> 'tap2 tap0 tap1'), then BRDGDELS('tap1') found the MEMBER on the span list -> 'panic: bridge_delete_span: not a span interface' (bridge_ioctl_delspan -> bridge_delete_span, cpuid 0). On non-INVARIANTS kernels the same chain silently unlinks and kfrees a live member bif (if_bridge still set; other cpus still hold their bifs) -> UAF/double-free at teardown; the impact on this INVARIANTS guest is the deterministic KASSERT panic.
No comments yet.