# DF-0590 — PoC: legacy netgraph/ng_bridge no-serialization SMP race

**Verdict: NOT REPRODUCED via userspace ng_socket injection (code-confirmed race).**
The race is **real in the code** (verified by line-by-line source trace — no lock,
no token, no `NG_NODE_FORCE_WRITER`; `NG_SEND_DATA` dispatches `rcvdata` inline) but
the PoC's attack vector (userspace `ng_socket` frame injection) **cannot win it on
this kernel** because of a subtle serialization detail the finding did not account
for: every `ng_socket` data send is dispatched through **CPU 0's netisr message
port** (`so->so_port = netisr_cpuport(0)` at `sys/kern/uipc_socket.c:259`), and the
1 Hz `ng_bridge_timeout` callout also runs on CPU 0 (armed in the constructor which
runs in that same CPU 0 netisr context). Both sides of the "race" therefore execute
on the **same CPU, serialized** — the concurrent-mutation window the finding
describes cannot open from userspace.

The race **would** manifest in the realistic threat model the finding describes
(a root-configured bridge connected to `ng_ether` on a real NIC with SMP traffic),
because `ether_input` dispatches incoming packets via per-flow CPU hashing
(`netisr_hashport` at `sys/net/if_ethersubr.c:1189`), so `ng_bridge_rcvdata` runs
concurrently on multiple CPUs in that topology. This PoC simply cannot drive that
path from userspace on this single-NIC VM.

See **VERDICT.md** for the full code-level proof and reachability analysis.

## Files

- `race.c` — flood+race driver: builds a 2-hook bridge graph via binary
  `NGM_*` control messages (no `ngctl`), floods broadcast Ethernet frames with
  unique source MACs from two independent injector socket nodes (each with its
  own data socket + drain thread) so `ng_bridge_put` runs at ~100 Kfps. Build:
  `cc -O2 -lpthread -o race race.c`. Run: `./race [seconds]` (must be root).
- `build.sh` / `run.sh` — exact reproducible build and run commands.
- `build.log` / `run.log` / `run.2.log` — full untrimmed outputs.
- `env.txt` — guest environment (uname, modules, INVARIANTS confirmation).
- `VERDICT.md` — full narrative: code-level proof, reachability caveat, fix.
- `fix.diff` — `git apply`-able per-node `struct lock` patch (`git apply --check`
  passes against the read-only `sys/` tree).
- `manifest.json` — machine-readable catalog.

## Build & run

```
./build.sh    # cc -O2 -lpthread -o race race.c
./run.sh      # ./race 20    (must be root; EPERM as unprivileged uid)
```

## Privilege model (verified)

Building a netgraph graph requires **root**: the `ng_socket` control socket is
gated by `caps_priv_check(SYSCAP_RESTRICTEDROOT)` at
`sys/netgraph/socket/ng_socket.c:172-173`. Verified on the guest: as unprivileged
uid 1001 (`maxx`), the first `socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL)` returns
`EPERM`. The CVSS vector's `PR:L` claim is **overstated** for the "user builds the
graph" attack path; the realistic exposure is (a) a root-configured bridge
processing frames from an untrusted/remote segment, or (b) a root-local race.

## Expected outcome on this kernel

The flood injects **~1–3 million frames** across two hooks in 20–40 s with zero
ENOBUFS failures (drain threads prevent backpressure). **No panic** occurs. The
guest stays up. This is expected — see VERDICT.md for why the race cannot fire
from this vector despite the code being genuinely unprotected.
