β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3529

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3529 Β· 3 files
FileTypeDescriptionSize
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
README.md readme asymmetric-locking analysis + trigger preconditions
↓ download 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) 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).

VERDICT.md verdict why untested, why filed
↓ download raw

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_testable
per-fix-DF-3529

Confirmed 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.