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)
PoC verification
Evidence pack
findings/poc/DF-0271 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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).
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_testablecompile 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.
No comments yet.