Span-port mirror path dereferences NULL softc: bridge_enqueue_handler passes span ifp's NULL if_bridge to bridge_handoff β deterministic kernel panic on first mirrored packet
Summary
bridge_span() mirrors packets via bridge_enqueue(span_ifp) (:3279); the async handler calls bridge_handoff(dst_ifp->if_bridge, ...) (:4430) and bridge_handoff immediately executes bifp = sc->sc_ifp (:4440). Span ports never get if_bridge set - only bridge_ioctl_add assigns it, addspan never does, and bridge_delete_span's own KASSERT documents the invariant. With a span port configured, the first packet through bridge_input/bridge_output - including the bridge's own IPv6 DAD output - panics the kernel. REPRODUCED first attempt: 'Fatal trap 12, fault VA 0x3b8 / Stopped at bridge_handoff+0x11: movq 0x3b8(%rdi),%rax' with rdi==NULL (offsetof(bridge_softc, sc_ifp)). The span feature is entirely unusable; any adjacent-host frame or local output kills the box once root configures a mirror port (unauth DoS of any host using port mirroring). Secondary vector: member deleted between enqueue and handler also yields NULL (DF-0271-family, distinct unchecked sink). Fix validated (mirror directly via bridge_handoff + NULL guard): 8 frames injected, guest stays up.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3276 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.0 KB | β raw | |
| verdict.json | β | 3.5 KB | view raw | |
| env.txt | β | 743 B | view raw | |
| build.log | β | 507 B | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| run.sh | β | 130 B | view raw | |
| panic.txt | β | 2.2 KB | view raw | |
| run.log | β | 1.6 KB | view raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| span_panic.c | β | 2.5 KB | view raw | |
| run_fixed.log | β | 1.2 KB | view raw |
DF-3276 β bridge span-port NULL dereference β deterministic kernel panic
What
bridge_span() hands mirrored packets to bridge_enqueue(), whose
netmsg handler dereferences dst_ifp->if_bridge β but span ports
never get if_bridge set (only member add does,
sys/net/bridge/if_bridge.c:1300; bridge_ioctl_addspan never assigns
it, and bridge_delete_span KASSERTs it is NULL,
sys/net/bridge/if_bridge.c:1152). The first packet mirrored to a span
port (or the first locally-originated packet once a span port exists)
panics the kernel:
bridge_input()/bridge_output()
-> bridge_span() if_bridge.c:3258
-> bridge_enqueue(span_ifp) if_bridge.c:3279
-> bridge_enqueue_handler() if_bridge.c:4418
bridge_handoff(dst_ifp->if_bridge /* == NULL */, ...)
-> bridge_handoff() if_bridge.c:4433
bifp = sc->sc_ifp; if_bridge.c:4440 <-- NULL+0x3b8 fault
Reproduce (guest, as root)
cd /root/poc/df-3276 cc -O -o span_panic span_panic.c sh run.sh # creates bridge0 + tap0/tap1/tap2, adds tap2 as span port, injects
Expected
Immediate kernel panic:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x3b8 Stopped at bridge_handoff+0x11: movq 0x3b8(%rdi),%rax # rdi == NULL (sc)
(0x3b8 = offsetof(struct bridge_softc, sc_ifp).)
In the captured baseline run the panic was triggered by the bridge's
own IPv6 DAD output (bridge_output() also calls bridge_span() at
if_bridge.c:2241/2358) right after the span port was configured β no
attacker-injected frame is even needed; any frame through the bridge
works. The tree is byte-identical to DragonFlyBSD master as of the
audit, so this is a live upstream bug.
Fix
fix.diff β mirror directly via bridge_handoff(sc, dst_if, mc, 0)
(also correct: span must not rewrite the source MAC), plus a defensive
NULL guard in bridge_enqueue_handler() for the member-deletion race.
DF-3276 β VERDICT
REPRODUCED β panic (deterministic).
Environment
Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026,
X86_64_GENERIC, 6 CPUs, stock kernel, if_bridge compiled in.
sys/net/bridge/if_bridge.c in this tree is byte-identical to
DragonFlyBSD master (verified by diff against the upstream GitHub raw
copy), so the bug is a live upstream defect, not an artifact of this
audit tree.
How it was reproduced (attempt 1 of 1 β first try panicked)
ifconfig bridge0 create,ifconfig tap0|tap1|tap2 create + up,ifconfig bridge0 addm tap0 addm tap1 up(run.sh).- PoC issues
SIOCSDRVSPEC/BRDGADDSto make tap2 a span port (sc->sc_span = 1, tap2->if_bridge stays NULL by design). - In the captured run the kernel panicked before any injected frame
was needed: the bridge interface's own IPv6 link-local DAD
transmission went through
bridge_output(), which callsbridge_span()for every output packet (if_bridge.c:2241, 2358).bridge_span()copied the frame and calledbridge_enqueue(tap2_span_ifp)(if_bridge.c:3279). The async netmsg handlerbridge_enqueue_handler()then calledbridge_handoff(dst_ifp->if_bridge, ...)withif_bridge == NULL(if_bridge.c:4430), andbridge_handoff()executedbifp = sc->sc_ifp(if_bridge.c:4440) on the NULL softc.
Panic (full text in panic.txt, from the serial console):
Fatal trap 12: page fault while in kernel mode cpuid = 0; lapic id = 0 fault virtual address = 0x3b8 instruction pointer = 0x8:0xffffffff82622d11 current process = Idle (netisr0 context) Stopped at bridge_handoff+0x11: movq 0x3b8(%rdi),%rax
%rdi = 0 (the NULL softc), 0x3b8 = offsetof(struct bridge_softc,
sc_ifp) β the disassembly pins the fault to bifp = sc->sc_ifp
exactly. The guest hung at db> and vm.sh status reported down.
Why span ports have if_bridge == NULL
- Only
bridge_ioctl_add()assignsifs->if_bridge = sc(if_bridge.c:1300). bridge_ioctl_addspan()never touchesif_bridge; it only checksifs->if_bridge != NULLto reject members (if_bridge.c:1807).bridge_delete_span()KASSERTsbif->bif_ifp->if_bridge == NULL("not a span interface", if_bridge.c:1152) β the kernel itself documents the invariant that makes the enqueue path crash.
Impact
- Configuring any span (mirror) port requires root (
BRDGADDSisBC_F_SUSER-gated), but once configured the panic is triggered by ANY frame: forwarded traffic, adjacent-host frames, or the bridge's own output (IPv6 DAD/NDP, ARP). The span feature is unusable β it panics the box on the first packet, deterministically. - Secondary vector (same sink): a member deleted between
bridge_enqueue()and the handler runs readsif_bridge == NULLtoo (cleared at if_bridge.c:1071 before the sync that drains pending enqueue msgs) β a race variant of DF-0271 with a different, unchecked sink.
Exploit chain
Not applicable (NULL-page read at fixed offset β panic). On this guest there is no way to map page 0, so this is a pure DoS/panic.
Fix validation
fix.diff applied to the guest's /usr/src copy; kernel rebuilt
(make nativekernel), rebooted; the identical PoC then ran to
completion with no panic and the mirrored frames appeared on the span
tap (see run_fixed.log). Baseline vs patched behavior recorded in
this pack (build.log / run.log / panic.txt vs 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 with fix_all.diff (patch -p1, make -j6 nativekernel, make installkernel, reboot). Identical PoC: 8 frames injected through the span-configured bridge, no panic, guest remains up and responsive (run_fixed.log). Baseline panic signature gone.
findings/poc/DF-3276/run_fixed.log; findings/poc/DF-3276/fix.diff; combined /root/build.log BUILD_DONE on guest
Confirmed kernel references
Detail
Evidence (decisive lines)
["panic.txt: 'Stopped at bridge_handoff+0x11: movq 0x3b8(%rdi),%rax' with %rdi=NULL, faulting address 0x3b8 (offsetof sc_ifp)", 'run.log: baseline ssh transcript ending in the panic', "run_fixed.log: patched kernel, 8 frames injected, 'still alive -- no panic observed', guest up", 'fix.diff: bridge_span direct handoff + enqueue-handler NULL guard']
PoC changes
Seed PoC (none existed for this fresh pass-2 finding) written from scratch: bridge/tap setup via ifconfig + BRDGADDS via raw SIOCSDRVSPEC (ifconfig has no spanport subcommand); frame type ETHERTYPE_ARP so ip_lengthcheck does not drop the frame pre-bridge.
Verified recommended fix
bridge_span: mirror via bridge_handoff(sc, dst_if, mc, 0) instead of bridge_enqueue(span_ifp); add NULL guard in bridge_enqueue_handler
Verdict
Deterministic kernel NULL-deref panic: bridge_span() hands mirrored packets to bridge_enqueue(), whose handler dereferences dst_ifp->if_bridge -- always NULL for span ports (only member add sets it, if_bridge.c:1300; bridge_delete_span KASSERTs the invariant, if_bridge.c:1152). Reproduced on the first attempt on the stock guest: with a span port configured, the bridge's own IPv6 DAD output via bridge_output()->bridge_span() panicked the box before any injected frame ('Fatal trap 12, fault virtual address 0x3b8, Stopped at bridge_handoff+0x11: movq 0x3b8(%rdi),%rax' with rdi==NULL; 0x3b8 == offsetof(bridge_softc, sc_ifp)). Any frame through a span-configured bridge triggers it. Tree is byte-identical to DragonFlyBSD master (live upstream bug). Fix (mirror via bridge_handoff(sc,dst_if,mc,0) + NULL guard in bridge_enqueue_handler) validated on a rebuilt kernel: same PoC injects 8 frames, guest stays up.
No comments yet.