Control-message request msg leaked on EBUSY early return in POINT2POINT/BROADCAST handler
| Field | Value |
|---|---|
| ID | DF-0613 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-401 Missing Release of Memory after Effective Lifetime |
| File | sys/netgraph/iface/ng_iface.c |
| Lines | 660-666 (bug); 728-733 (skipped epilogue) |
| Area | netgraph (legacy interface node control message handler) |
| Confidence | certain |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
When NGM_IFACE_POINT2POINT or NGM_IFACE_BROADCAST is sent while the
interface is UP, ng_iface_rcvmsg does return (EBUSY) directly at line
666, bypassing the function's cleanup epilogue at lines 728-733. Per the
netgraph framework contract (the CALL_MSG_HANDLER macro at
sys/netgraph/netgraph/ng_base.c:1186-1201 with the comment "It is up to
the message handler to free the message"), the rcvmsg handler owns msg
and must free it on every path. The early return therefore leaks the
request message. Repeated calls exhaust kmalloc(M_NETGRAPH) and
panic/OOM the system.
Root cause
sys/netgraph/iface/ng_iface.c:660-680:
660: case NGM_IFACE_POINT2POINT:
661: case NGM_IFACE_BROADCAST:
662: {
663:
664: /* Deny request if interface is UP */
665: if ((ifp->if_flags & IFF_UP) != 0)
666: return (EBUSY); /* <-- leaks msg, skips kfree at 732 */
...
679: break;
680: }
The function epilogue at 728-733:
728: if (rptr)
729: *rptr = resp;
730: else if (resp)
731: kfree(resp, M_NETGRAPH);
732: kfree(msg, M_NETGRAPH);
733: return (error);
The early return (EBUSY) at line 666 short-circuits both the resp
handling (resp is still NULL here, so no double-free) and the
kfree(msg). The leaked buffer is sizeof(struct ng_mesg) +
msg->header.arglen, where arglen is fully attacker-controlled (see
ngc_send at sys/netgraph/socket/ng_socket.c:248-258, which kmallocs
len + 1 from the user-supplied mbuf chain).
The correct pattern is shown by ng_eiface_rcvmsg
(sys/netgraph/eiface/ng_eiface.c:390-494) which always uses
error = ...; break; and lets the epilogue clean up.
Threat model & preconditions
- Attacker position: any local user who can open an
NG_CONTROLsocket.sys/netgraph/socket/ng_socket.c:172gatesngc_attachbehindcaps_priv_check(SYSCAP_RESTRICTEDROOT | __SYSCAP_NULLCRED), so this is in practice a root-only path. - Privileges gained or impact: true kernel memory leak, exploitable
for unbounded kernel-memory-exhaustion DoS. The leaked buffer size is
attacker-controlled (sized by
msg->header.arglen). - Trigger: create an
ng_ifacenode, bring the interface UP (ifconfig ng0 uporSIOCSIFFLAGS), then sendNGM_IFACE_POINT2POINT/NGM_IFACE_BROADCASTviaNGCTL sendmsgin a tight loop. Each iteration leaks the request buffer.
Proof of concept
PoC source: findings/poc/DF-0613/leak_ng_iface.c.
Build & run
cc -o leak_ng_iface leak_ng_iface.c # as root, on a host with an ng_iface node 'ng0' brought UP: ./leak_ng_iface
Expected output
vmstat -m shows M_NETGRAPH climbing monotonically; eventually the
system panics with kmem_malloc: out of space or hangs. The leak is
exactly sizeof(struct ng_mesg) + msg->header.arglen per iteration; for
fastest exhaustion, set arglen to a large value via the socket mbuf size.
Impact
- Blast radius: any DragonFlyBSD host with the
netgraph_ifacemodule loaded and a configured interface that is UP. - Severity rationale: Low. Root-only trigger (
SYSCAP_RESTRICTEDROOT), impact is memory-exhaustion DoS only (no corruption primitive). - Reliability: 100% per call β no race, deterministic leak.
Recommended fix
Replace the early return (EBUSY) with the standard error = EBUSY;
break; so the epilogue at lines 728-733 still frees msg.
--- a/sys/netgraph/iface/ng_iface.c
+++ b/sys/netgraph/iface/ng_iface.c
@@ -662,8 +662,9 @@ ng_iface_rcvmsg(node_p node, struct ng_mesg *msg,
{
/* Deny request if interface is UP */
- if ((ifp->if_flags & IFF_UP) != 0)
- return (EBUSY);
+ if ((ifp->if_flags & IFF_UP) != 0) {
+ error = EBUSY;
+ break;
+ }
/* Change flags */
switch (msg->header.cmd) {
References
sys/netgraph/iface/ng_iface.c:728-733β the epilogue that freesmsg, skipped by the early return.sys/netgraph/netgraph/ng_base.c:1186-1201βCALL_MSG_HANDLER, the framework contract that the handler ownsmsg.sys/netgraph/eiface/ng_eiface.c:390-494β the sibling handler that correctly useserror = ...; break;on every path.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0613 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_ng_iface.c | trigger-source | PoC: creates ng_iface node, brings UP, spams NGM_IFACE_POINT2POINT to trigger EBUSY leak | 6.4 KB | view raw |
| fix.diff | suggested-fix | git-apply-able: return(EBUSY) β error=EBUSY; break; at ng_iface.c:665-666 | 416 B | view raw |
| build.sh | build-script | compiles leak_ng_iface.c | 148 B | view raw |
| run.sh | run-script | loads modules, runs PoC, shows vmstat before/after | 484 B | view raw |
| build.log | build-log | final successful build output | 119 B | view raw |
| run.log | run-log | BEFORE test: EBUSY bug present, M_NETGRAPH 1.11Kβ112Kβ222K | 1.7 KB | view raw |
| fix_run.log | fix-run-log | AFTER test: EBUSY fix applied, M_NETGRAPH stays flat (1.11Kβ1.11Kβ2.2K) | 1.6 KB | view raw |
| env.txt | environment | uname, cc version, noinv kernel rationale, module loading notes | 1.6 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism, privilege boundary, runtime blockers, before/after | 4.3 KB | β raw |
| README.md | readme | PoC overview and build/run instructions | 1.7 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0613 β PoC: ng_iface rcvmsg msg leak on EBUSY early return
Root-only local kernel-memory-exhaustion DoS PoC.
Files
leak_ng_iface.cβ creates anNG_CONTROLsocket, creates anng_ifacepeer node viaNGM_MKPEER, queries its interface name viaNGM_IFACE_GET_IFNAME, brings it UP viaSIOCSIFFLAGS, then spamsNGM_IFACE_POINT2POINTcontrol messages in a tight loop. Each iteration leaks thesizeof(struct ng_mesg) + arglenrequest buffer becauseng_iface_rcvmsgdoesreturn (EBUSY)at line 666, bypassing thekfree(msg)at line 732.fix.diffβ replacesreturn (EBUSY)witherror = EBUSY; break;so the epilogue at lines 728-733 freesmsg.build.shβ compiles the PoC.run.shβ loads modules, runs the PoC, shows vmstat -m before/after.
Build & run
cc -O2 -Wall -o leak_ng_iface leak_ng_iface.c # as root, on a host with netgraph + ng_socket + ng_iface loaded: ./leak_ng_iface [iterations] [arglen]
Expected outcome
vmstat -m | grep netgraph shows M_NETGRAPH climbing monotonically
(each iteration leaks ~52 bytes with arglen=0). Eventually the system
panics with kmem_malloc: out of space or hangs.
With the fix applied, M_NETGRAPH stays flat β the messages are freed by the epilogue.
Runtime testing notes
The default GENERIC kernel (#0, INVARIANTS ON) has two separate bugs that prevent creating ng_iface nodes at runtime (see VERDICT.md and env.txt). Fix validation was performed on the noinv-installed snapshot (INVARIANTS OFF) with a test-only constructor fix applied to the ng_iface module. The DF-0613 fix.diff contains only the EBUSY returnβbreak change.
DF-0613 β VERDICT
Verdict: REPRODUCED (resource leak / DoS)
Impact: dos (root-only kernel-memory-exhaustion via M_NETGRAPH leak)
Confidence: certain
Mechanism
When NGM_IFACE_POINT2POINT or NGM_IFACE_BROADCAST is sent to an
ng_iface node while the interface is UP, ng_iface_rcvmsg() at
sys/netgraph/iface/ng_iface.c:665-666 does:
if ((ifp->if_flags & IFF_UP) != 0)
return (EBUSY); /* <-- leaks msg, skips kfree at 732 */
This return (EBUSY) bypasses the function's cleanup epilogue at
lines 728-733:
if (rptr)
*rptr = resp;
else if (resp)
kfree(resp, M_NETGRAPH);
kfree(msg, M_NETGRAPH); /* <-- skipped by early return */
return (error);
Per the netgraph framework contract (sys/netgraph/netgraph/ng_base.c:1178:
"It is up to the message handler to free the message"), the rcvmsg
handler owns msg and must free it on every path. The early
return leaks sizeof(struct ng_mesg) + msg->header.arglen bytes of
M_NETGRAPH per call.
The leaked buffer size is attacker-controlled (sized by
msg->header.arglen, set from the user-supplied mbuf chain in
ngc_send at sys/netgraph/socket/ng_socket.c:248-258).
Privilege boundary
The NG_CONTROL socket is root-only: ngc_attach at
sys/netgraph/socket/ng_socket.c:172-173 gates on
caps_priv_check(SYSCAP_RESTRICTEDROOT | __SYSCAP_NULLCRED).
This is a rootβkernel DoS, not an unprivileged escalation.
Exploitation ceiling
Resource leak β memory exhaustion β DoS. No corruption primitive,
no privilege escalation. Repeated calls exhaust M_NETGRAPH and
panic/OOM the system. Severity: Low (root-only, DoS only).
Runtime reproduction
The leak was confirmed at runtime on the noinv-installed snapshot
(INVARIANTS OFF). The default GENERIC kernel (#0, INVARIANTS ON) has
two separate bugs that prevent creating ng_iface nodes at runtime:
-
kfree(NULL) in ng_iface_get_unit():
bcopy()is declared__nonnull(1,2)atsys/sys/systm.h:280. The compiler (gcc 8.3) uses this to eliminate theif (ng_iface_units != NULL)check beforekfree()atng_iface.c:294, causing a panic on the firstng_iface_get_unit()call whenng_iface_unitsis still NULL. -
ifnet_lock KASSERT in if_attach():
ifnet_lock()hasKASSERT(td_type != TD_TYPE_NETISR)atsys/net/if.c:3787. The ng_socket send path runs in a netisr thread context, song_iface_constructor()callingif_attach()triggers the KASSERT (INVARIANTS-gated, only on GENERIC).
A test-only constructor fix (changing if (ng_iface_units != NULL)
to if (ng_iface_units_len > 0)) was applied to both the BEFORE and
AFTER ng_iface.ko modules to work around issue (1). Issue (2) is
bypassed on the noinv kernel (KASSERT is INVARIANTS-gated).
Before (EBUSY bug present):
BEFORE vmstat: netgraph 5 1.11K 0 390M 18 β 2000 NGM_IFACE_POINT2POINT messages sent AFTER vmstat: netgraph 1.96K 112K 0 390M 5.89K β M_NETGRAPH grew by ~111K (2000 Γ ~52 bytes/msg leaked)
After (EBUSY fix applied):
BEFORE vmstat: (empty) β 2000 NGM_IFACE_POINT2POINT messages sent AFTER vmstat: netgraph 5 1.11K 0 390M 5.88K β M_NETGRAPH stayed at 1.11K β NO LEAK (messages freed by epilogue) Run 2 (determinism): β 2000 more messages AFTER vmstat: netgraph 9 2.20K 0 390M 11.8K β grew by only ~1K (new node overhead), NOT 112K β NO LEAK
Fix
fix.diff replaces the early return (EBUSY) at line 666 with
error = EBUSY; break; so the epilogue at lines 728-733 frees msg:
- if ((ifp->if_flags & IFF_UP) != 0)
- return (EBUSY);
+ if ((ifp->if_flags & IFF_UP) != 0) {
+ error = EBUSY;
+ break;
+ }
This matches the finding markdown's ## Recommended fix proposal.
The fix was validated: on the noinv kernel with the fix applied,
2000+2000 POINT2POINT messages produced zero M_NETGRAPH growth
(vs ~222K leaked without the fix).
PoC changes
- Wrote
leak_ng_iface.c(the finding had only a README; no source existed). The PoC creates an ng_iface node via NGM_MKPEER, queries its name via NGM_IFACE_GET_IFNAME, brings it UP via SIOCSIFFLAGS, then spams NGM_IFACE_POINT2POINT messages. Each call triggers the EBUSY early-return leak path. - Wrote
fix.diff(standalone git-apply-able diff). - Wrote
build.sh,run.sh.
Fix verification
fixedVALIDATED: baseline 2000 msgs -> 112K leaked; patched -> 0 leaked. Deterministic.
BEFORE: 1.11K->112K (2000 msgs, ~111K leaked). AFTER: 1.11K->1.11K (2000 msgs, NO LEAK).
Confirmed kernel references
Detail
Exploit chain
none -- resource leak (CWE-401). Root-only DoS via kernel-memory exhaustion.
Evidence (decisive lines)
baseline: 2000 msgs -> M_NETGRAPH 1.11K->112K (leaked ~111K). patched: 2000 msgs -> 1.11K->1.11K (NO LEAK).
PoC changes
Wrote leak_ng_iface.c from scratch. Added fix.diff, build.sh, run.sh, VERDICT.md, manifest.json, full logs.
Verified recommended fix
Replace early return(EBUSY) at ng_iface.c:666 with error=EBUSY; break; so epilogue frees msg. Matches finding proposal. Full git-apply-able diff in findings/poc/DF-0613/fix.diff.
Verdict
REPRODUCED. ng_iface_rcvmsg:665-666 does return(EBUSY) when interface is UP, bypassing epilogue at :728-733 that frees msg via kfree(msg, M_NETGRAPH). Each call leaks ~52 bytes. Confirmed: 2000 messages grew M_NETGRAPH 1.11K->112K (leaked ~111K), deterministic. Root-only (NG_CONTROL socket).
No comments yet.