โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0591

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_bridge node 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 shutdown on peer hooks to tear down all but one link, or
  • normal lifecycle where peer hooks are detached (e.g. an ng_ether partner 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_bridge reachable 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 == 1 state (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.

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 netgraph7 firstLink reservation pattern that correctly always consumes the original mbuf on the final send.
  • Related historical fix: FreeBSD ng_bridge for 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).