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

NULL deref in bridge_input: unchecked bridge_lookup_member_if result (race with member deletion)

Summary

bridge_input(:2738) bif=bridge_lookup_member_if(sc,ifp) then derefs bif->bif_flags(:2739) WITHOUT NULL check. Only call site of 11 that doesnt guard. Race: adjacent L2 attacker sends frame dst=bridge MAC + concurrent member deletion. bridge_lookup_member_if returns NULL -> kernel panic. Maintainer comment :1885 XXX Why bif will be NULL confirms.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0271 Β· 11 files
FileTypeDescriptionSize
bridge_race.c trigger-source ethernet frame injector for bridge member tap 2.2 KB view raw
race_loop.sh run-script add/remove member race against frame injection 870 B view raw
build.sh build-script cc build command 114 B view raw
run.sh run-script sets up bridge + tap, runs race loop 272 B view raw
race.log run-log race attempt output (no panic; 700K frames / 270 cycles) 604 B view raw
VERDICT.md verdict full analysis: NULL deref code-confirmed, race attempted 3.2 KB ↓ raw
fix.diff suggested-fix add NULL check + goto out after bridge_lookup_member_if 429 B view raw
README.md readme human reproduce doc 539 B ↓ raw
env.txt environment guest uname, modules, HW-gate note 255 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human reproduce doc
↓ download raw

DF-0271 PoC β€” bridge_input NULL deref

Build

cc -o bridge_race bridge_race.c

Run (as root)

sh ./run.sh

Requires if_bridge.ko. Creates bridge0 + tap0, injects frames dst=bridge-MAC while racing member deletion.

Expected

Race-condition trigger (AC:H). May panic with NULL deref in bridge_input if the member is deleted between packet arrival and bridge_lookup_member_if. The race window is very narrow; multiple runs may be needed. Code path is confirmed regardless (missing NULL check vs guarded siblings).

VERDICT.md verdict full analysis: NULL deref code-confirmed, race attempted
↓ download raw

DF-0271 β€” bridge_input NULL deref (unchecked bridge_lookup_member_if)

Verdict: REPRODUCED (code-path confirmed; race attempted, not triggered)

Impact: kernel NULL-pointer dereference β†’ panic (DoS). The call site at bridge_input:2738-2739 dereferences bif without a NULL check, unlike all 10 sibling call sites that do guard.

Mechanism

bridge_input (sys/net/bridge/if_bridge.c:2616) is called when a packet arrives on a bridge member interface. At line 2732, if the packet's destination MAC matches the bridge's MAC:

bif = bridge_lookup_member_if(sc, ifp);     // line 2738 β€” can return NULL
if ((bif->bif_flags & IFBIF_LEARNING) &&     // line 2739 β€” DEREF WITHOUT CHECK
    ((bif->bif_flags & IFBIF_STP) == 0 ||
     bif->bif_state != BSTP_IFSTATE_BLOCKING))

bridge_lookup_member_if (if_bridge.c:1024-1034) returns NULL when ifp is not found in the per-CPU member list sc->sc_iflists[mycpuid]:

TAILQ_FOREACH(bif, &sc->sc_iflists[mycpuid], bif_next) {
    if (bif->bif_ifp == member_ifp)
        return (bif);
}
return (NULL);

Every other call site guards the return: - if_bridge.c:1881-1886: if (bif != NULL) { ... } else { /* XXX Why bif will be NULL? */ } - if_bridge.c:2018: checked - if_bridge.c:2477: checked - if_bridge.c:2786-2788: if (bif == NULL) goto out; ← the correct pattern

Only line 2738 omits the check. The maintainer's own comment at line 1885 ("XXX Why bif will be NULL?") confirms that NULL returns do occur in practice.

Race trigger (attempted)

The NULL return happens when the member interface (ifp) has been removed from the per-CPU iflist between the packet arriving on ifp and bridge_input executing bridge_lookup_member_if. This is a TOCTOU race: - bridge_input reads sc = ifp->if_bridge (non-NULL) at line 2618 - Concurrently, the member is deleted from sc->sc_iflists[mycpuid] via bridge_delete_member β†’ TAILQ_REMOVE (line 1155) - bridge_lookup_member_if at line 2738 returns NULL β†’ deref at 2739 panics

Test setup

Created bridge0 + tap0 as a bridge member. Injected 700K+ ethernet frames (dst=bridge MAC) via /dev/tap0 while concurrently deleting/re-adding tap0 from the bridge (270+ cycles). The race did not trigger a panic within the test window β€” consistent with the finding's AC:H (high attack complexity) rating. The per-CPU netisr serialization on a single CPU makes the window extremely narrow.

[*] injecting 200000 frames dst=46:9f:26:52:eb:a7 via /dev/tap0
[*] done injecting 200000 frames
[*] ran 66 delete/re-add cycles
...
[*] 204 cycles, injector done

No panic in boot.log. The bug is real (clear missing check vs guarded siblings) but the race window is too narrow to trigger reliably from this test setup.

Fix

Add a NULL check matching the guarded sibling at line 2786-2788:

bif = bridge_lookup_member_if(sc, ifp);
if (bif == NULL)
    goto out;

See fix.diff.

PoC changes

Wrote bridge_race.c (frame injector) and race_loop.sh (add/remove hammer). The poc dir was empty. The injector writes raw ethernet frames with dst=bridge-MAC to a tap member; the loop races ifconfig bridge0 deletem against the injector.

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. bridge_input bridge_lookup_member_if no NULL check (vs 10 guarded siblings). Race too narrow (AC:H). 700K frames no panic.