β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3276

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3276 Β· 11 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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)

  1. ifconfig bridge0 create, ifconfig tap0|tap1|tap2 create + up, ifconfig bridge0 addm tap0 addm tap1 up (run.sh).
  2. PoC issues SIOCSDRVSPEC/BRDGADDS to make tap2 a span port (sc->sc_span = 1, tap2->if_bridge stays NULL by design).
  3. 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 calls bridge_span() for every output packet (if_bridge.c:2241, 2358). bridge_span() copied the frame and called bridge_enqueue(tap2_span_ifp) (if_bridge.c:3279). The async netmsg handler bridge_enqueue_handler() then called bridge_handoff(dst_ifp->if_bridge, ...) with if_bridge == NULL (if_bridge.c:4430), and bridge_handoff() executed bifp = 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() assigns ifs->if_bridge = sc (if_bridge.c:1300).
  • bridge_ioctl_addspan() never touches if_bridge; it only checks ifs->if_bridge != NULL to reject members (if_bridge.c:1807).
  • bridge_delete_span() KASSERTs bif->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 (BRDGADDS is BC_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 reads if_bridge == NULL too (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

fixed
baseline reproduced→ patch + rebuild →patched clean

Rebuilt 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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 10 00:48:01 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.