ng_ether control-plane teardown frees priv without synchronizing against BGL-held data-path callbacks (speculative UAF)
Summary
NGM_ETHER_DETACH -> ng_ether_detach runs from the netgraph item dispatcher with no BGL; it clears IFP2NG/priv->ifp and via ng_rmnode_self -> ng_ether_shutdown REALLY_DIE path kfrees priv. Meanwhile ng_ether_output/input/input_orphan read NG_NODE_PRIVATE(node) once and dereference priv->upper/lower/orphan/ifp while if_ethersubr holds the BGL around them. No lock is shared between the two paths; the source itself flags the window ('XXX race if interrupted an output packet'). A lost race is a read/use of freed M_NETGRAPH memory plus a data send through a freed hook pointer. Root-only on current kernels (control socket gated by caps_priv_check); becomes an unprivileged UAF primitive the day the ng control plane is exposed below root. Not executed this pass (status untested); priv-bypass on setpromisc/setenaddr KILLED at socket layer.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3529 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | asymmetric-locking analysis + trigger preconditions | 2.7 KB | β raw |
| VERDICT.md | verdict | why untested, why filed | 2.2 KB | β raw |
| verdict.json | verdict | machine verdict | 2.1 KB | view raw |
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) readsNG_NODE_PRIVATE(node)once and then dereferencespriv->upper,priv->ifpetc. -
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 thenng_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).
DF-3529 VERDICT
status: untested β speculative, root-gated UAF race; not run this pass.
Reproduced?
No. Not attempted: reaching the window requires a root-held netgraph control
socket (ngc_attach: caps_priv_check(SYSCAP_RESTRICTEDROOT),
sys/netgraph7/socket/ng_socket.c:182-185) firing NGM_ETHER_DETACH at the
exact moment the target interface's data path is between
NG_NODE_PRIVATE(node) and its uses inside ng_ether_output/ng_ether_input
(ng_ether.c:219-227, 255-268) / their orphan twin (ng_ether.c:236-248).
A deterministic trigger is not available to an unprivileged user; a fuzzing
campaign (root: loop DETACH vs. flood traffic) might hit it but was out of
scope for this Low-severity speculative item.
Why filed at all
The code itself flags the window: ng_ether.c:326
priv->ifp = NULL; /* XXX race if interrupted an output packet */.
The locking is provably asymmetric:
- data-path call sites hold the BGL (sys/net/if_ethersubr.c:389-405 and
1160-1176: get_mplock() around ng_ether_output_p / ng_ether_input_p /
ng_ether_input_orphan_p);
- the control-plane teardown (NGM_ETHER_DETACH β ng_ether_detach,
ng_ether.c:313-328 β ng_ether_shutdown ng_ether.c:677-688 kfree(priv))
runs from ng_apply_item with no BGL and no private-data lock.
If lost, CPU0 uses freed priv (objcache M_NETGRAPH) β a UAF read plus a
data send through a freed hook pointer. Severity capped at Low: root-gated,
narrow window, never demonstrated.
Fix validation
n/a (nothing built). Suggested fix in README.md (BGL in ng_ether_detach, or
a shared lock/reference over priv).
Bottom line
Genuine missing-synchronization pattern with the author's own XXX on it; keep as hardening. Anyone making the netgraph control plane reachable to unprivileged users (e.g. relaxing ngc_attach) must fix this first.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md + README.md: locking asymmetry citations (if_ethersubr.c:393,1175 get_mplock vs ng_apply_item no-BGL), teardown free path ng_ether.c:684-686
PoC changes
n/a (no PoC authored)
Verified recommended fix
Take the BGL (or a private-data rwlock/reference) across ng_ether_detach's IFP2NG/priv->ifp invalidations and ng_rmnode_self, making teardown symmetric with the data path.
Verdict
Speculative, root-gated use-after-free race: ng_ether's data-path callbacks run under the BGL (if_ethersubr.c:393/1175) but the NGM_ETHER_DETACH teardown (ng_ether.c:313-328 -> ng_ether_shutdown REALLY_DIE kfree(priv) ng_ether.c:684-686) runs from the netgraph item dispatcher with no BGL and no shared lock, so a control-message detach can free struct private while another CPU is between its NG_NODE_PRIVATE(node) load and its uses in ng_ether_output/input (ng_ether.c:219/255/596/640); the code's own comment flags it (ng_ether.c:326 'XXX race if interrupted an output packet'). Not demonstrated: reaching NGM_ETHER_DETACH needs a control socket gated by caps_priv_check(SYSCAP_RESTRICTEDROOT) (ng_socket.c:182), and the window is a single load-to-use span; no fuzzing campaign was run for this Low item.
No comments yet.