# DF-3529 — ng_ether: control-plane detach frees `priv` without synchronizing
against BGL-held data-path callbacks (speculative UAF race)

## What
Two paths touch `struct private` with **asymmetric locking**:

- **Data path** (ether_output → ng_ether_output; ether_input →
  ng_ether_input / ng_ether_input_orphan): the if_ethersubr.c call sites
  hold the BGL around the callback — if_ethersubr.c:393 (`get_mplock()` for
  ng_ether_output_p) and if_ethersubr.c:1175 (for ng_ether_input_orphan_p).
  Inside, ng_ether_output (ng_ether.c:255-268) reads
  `NG_NODE_PRIVATE(node)` once and then dereferences `priv->upper`,
  `priv->ifp` etc.

- **Control path** (NGM_ETHER_DETACH, ng_ether.c:552-554 →
  ng_ether_detach, ng_ether.c:313-328): runs from the netgraph item
  dispatcher (ng_apply_item) with **no BGL**. It sets `IFP2NG(ifp) = NULL`,
  `priv->ifp = NULL` (the code's own `/* XXX race if interrupted an output
  packet */` at ng_ether.c:326 admits the window) and then
  `ng_rmnode_self(node)` → ng_ether_shutdown REALLY_DIE path
  (ng_ether.c:677-688) → `kfree(priv, M_NETGRAPH)`.

CPU0 inside ng_ether_output/ng_ether_input holds only the BGL; CPU1
processing a DETACH control message holds no lock in common with it. If
CPU1 frees `priv` while CPU0 is between its `NG_NODE_PRIVATE(node)` load
and its uses of `priv->upper` / `priv->lower` / `priv->orphan` /
`priv->ifp`, CPU0 dereferences freed memory (read + the subsequent
`NG_SEND_DATA_ONLY(error, priv->upper, *mp)` uses a freed hook pointer).
The freed `priv` is M_NETGRAPH (objcache), so a groomed reallocation makes
this a use-after-free with attacker-influenced contents.

Same shape applies to `ng_ether_rcv_upper/rcv_lower` (data items from netgraph
peers — no BGL at all there, only netgraph item references, which protect the
node, not `priv`: shutdown frees priv regardless).

## Preconditions (why Low / speculative)
- Reaching NGM_ETHER_DETACH requires a netgraph control socket; ng7's
  ngc_attach gates that with `caps_priv_check(SYSCAP_RESTRICTEDROOT)`
  (sys/netgraph7/socket/ng_socket.c:182-185) → **root only** on this guest.
- The window is one pointer-load-to-use span on a hot data path; winning the
  race needs DETACH + concurrent traffic, i.e. a fuzzing campaign a root
  attacker could run, but no demonstration was attempted this pass.

Not re-reported / separate from: DF-0617 (rcv_upper bridge_input_p UAF —
deterministic, different site).

## Suggested fix
Make the control-plane teardown symmetric with the data path: acquire the
BGL in ng_ether_detach before clearing IFP2NG/priv->ifp and calling
ng_rmnode_self (or convert ng_ether_{input,output} to take a private-data
reference under a lock the teardown also takes).
