# 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:

```c
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]`:

```c
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:
```c
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.
