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

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_CONTROL socket. sys/netgraph/socket/ng_socket.c:172 gates ngc_attach behind caps_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_iface node, bring the interface UP (ifconfig ng0 up or SIOCSIFFLAGS), then send NGM_IFACE_POINT2POINT/NGM_IFACE_BROADCAST via NGCTL sendmsg in 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_iface module 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.

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

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0613 Β· 12 files
FileTypeDescriptionSize
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
README.md readme PoC overview and build/run instructions
↓ download 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 an NG_CONTROL socket, creates an ng_iface peer node via NGM_MKPEER, queries its interface name via NGM_IFACE_GET_IFNAME, brings it UP via SIOCSIFFLAGS, then spams NGM_IFACE_POINT2POINT control messages in a tight loop. Each iteration leaks the sizeof(struct ng_mesg) + arglen request buffer because ng_iface_rcvmsg does return (EBUSY) at line 666, bypassing the kfree(msg) at line 732.
  • fix.diff β€” replaces return (EBUSY) with error = EBUSY; break; so the epilogue at lines 728-733 frees msg.
  • 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.

VERDICT.md verdict full narrative: mechanism, privilege boundary, runtime blockers, before/after
↓ download raw

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:

  1. kfree(NULL) in ng_iface_get_unit(): bcopy() is declared __nonnull(1,2) at sys/sys/systm.h:280. The compiler (gcc 8.3) uses this to eliminate the if (ng_iface_units != NULL) check before kfree() at ng_iface.c:294, causing a panic on the first ng_iface_get_unit() call when ng_iface_units is still NULL.

  2. ifnet_lock KASSERT in if_attach(): ifnet_lock() has KASSERT(td_type != TD_TYPE_NETISR) at sys/net/if.c:3787. The ng_socket send path runs in a netisr thread context, so ng_iface_constructor() calling if_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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 noinv with ng_iface.ko rebuilt with fix.diff (SHA256 d091a8d2...)

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