Legacy netgraph/ng_bridge leaks mbuf+meta when the bridge has exactly one link (numLinks==1 fan-out loop never runs)
| Field | Value |
|---|---|
| ID | DF-0591 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-401 Missing Release of Memory after Effective Lifetime |
| File | sys/netgraph/bridge/ng_bridge.c |
| Lines | 663-709 |
| Area | netgraph (legacy Ethernet bridge node) |
| Confidence | certain |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
When a legacy ng_bridge node has exactly one connected link
(numLinks == 1), any packet that reaches the "distribute to all other
links" fan-out loop in ng_bridge_rcvdata is never freed: the loop guard is
i < priv->numLinks - 1 which evaluates to i < 0 (false), so the loop
body โ the only place the original m is consumed (the "last link" branch
m2 = m at :674) โ never executes. The function then falls through to
return (error) at :709 without ever calling NG_FREE_DATA(m, meta). Each
leaked mbuf is an attacker-driven kernel memory allocation, enabling
memory-exhaustion DoS at one mbuf per injected broadcast / multicast /
unknown-unicast frame.
Root cause
ng_bridge_rcvdata reaches the fan-out block (sys/netgraph/bridge/ng_bridge.c:662-708)
for unknown-unicast, multicast and broadcast destinations, after the
early-return unicast-delivery path at :636-656.
The loop header at line 663 is:
663: for (linkNum = i = 0; i < priv->numLinks - 1; linkNum++) {
priv->numLinks (struct field at :100) is the total number of connected
links including the incoming link. With only the incoming link connected,
numLinks == 1, so priv->numLinks - 1 == 0 and the condition
i < 0 (with i initialized to 0) is false on the first iteration. The loop
body โ which is the only place the original m is consumed (the "last link"
branch m2 = m at :674) โ never runs.
The function then falls through to return (error) at :709 without ever
calling NG_FREE_DATA(m, meta) for the unconsumed mbuf. There is no
post-loop cleanup for the not-consumed case.
Contrast the netgraph7 version (sys/netgraph7/bridge/ng_bridge.c:700-740),
which reserves a firstLink so the original m is always consumed on
the final send โ the legacy code missed this pattern.
For numLinks >= 2 the bug does not trigger because at least one other link
is always found and the "last link" branch consumes m.
Threat model & preconditions
- Attacker position: any local user with netgraph access (
ng_socket,ngctl,ksocket). No special privileges beyond netgraph. - Privileges gained or impact: kernel memory exhaustion / DoS. mbufs are a finite kernel resource; sustained injection exhausts the mbuf pool and hangs network I/O system-wide. No info leak, no code execution.
- Required config or capabilities: an
ng_bridgenode that has been reduced to a single connected link. This is reachable via: - operator misconfiguration of a single-link bridge,
- an attacker who can issue
ngctl shutdownon peer hooks to tear down all but one link, or - normal lifecycle where peer hooks are detached (e.g. an
ng_etherpartner interface going down). - Reachability: send any frame that hits the fan-out path into the
single link โ i.e. any broadcast (
ff:ff:ff:ff:ff:ff), any multicast, or any unicast whose destination MAC is not currently in the host table.
Proof of concept
PoC source: findings/poc/DF-0591/leak.c
Build & run
# One-time topology: a single-link bridge ngctl mkpeer ng_iface0 bridge ether link0 ngctl name ng_iface0:ether br0 # (only link0 is connected; numLinks == 1) cc -O2 -o leak leak.c ./leak ng_iface0
In another shell, watch the mbuf count climb without bound:
netstat -m vmstat -z | grep mbuf
Expected output
netstat -m "mbufs in use" climbs monotonically (one mbuf per injected
broadcast frame). After enough frames the mbuf zone is exhausted and the
kernel reports allocation failures / hangs network I/O system-wide:
mbuf zone exhausted network output stalls ...
Impact
- Blast radius: any DragonFly system running a single-link legacy
ng_bridgereachable by an attacker with netgraph access. Realistic in VPN concentrators and lab setups where a bridge is provisioned but peer links have not yet been attached (or have been detached). - Severity rationale: Low. Reliable memory leak / DoS, but requires the
bridge to be in a
numLinks == 1state (not the steady-state for a working bridge). No info leak, no code execution. CVSS 3.1 base โ 3.8 (Low). - Reliability: 100% โ straight-line leak, no race.
Recommended fix
Free the unconsumed mbuf after the fan-out loop if it was not handed off. Minimal fix:
--- a/sys/netgraph/bridge/ng_bridge.c
+++ b/sys/netgraph/bridge/ng_bridge.c
@@ -706,6 +706,10 @@
/* Send packet */
NG_SEND_DATA(error, destLink->hook, m2, meta2);
}
+ if (m != NULL) { /* nobody consumed the original (e.g. numLinks == 1) */
+ NG_FREE_DATA(m, meta);
+ error = 0;
+ }
return (error);
}
Alternatively, adopt the netgraph7 firstLink reservation pattern
(sys/netgraph7/bridge/ng_bridge.c:700-740) which guarantees the original
mbuf is always consumed on the final send. This fix should be applied under
the same per-node lock proposed in DF-0590 once that fix is in place (the
fan-out loop runs concurrently with disconnect/newhook in the unlocked
legacy code, so a fully-correct fix requires both).
References
sys/netgraph7/bridge/ng_bridge.c:700-740โ the netgraph7firstLinkreservation pattern that correctly always consumes the original mbuf on the final send.- Related historical fix: FreeBSD
ng_bridgefor the same class of fan-out mbuf leak.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).