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

ng_eiface_rcvdata returns ENETDOWN without freeing the mbuf β€” permanent 1:1 mbuf+cluster leak per packet delivered to a DOWN interface (unbounded kernel heap exhaustion)

Summary

v1 rcvdata owns the mbuf on every path (ng_send_data hands over ownership and NG_SEND_DATA callers NULL their pointers), but ng_eiface_rcvdata's interface-down early return leaks it. VERIFIED live: 500x2048-byte packets to the ether hook of a DOWN ngethX -> exactly +500 mbuf clusters (256->756 in use); interface-UP control shows zero growth; the ng7 twin frees explicitly (NG_FREE_M before the identical ENETDOWN return), confirming the deviation from convention. Reachable behind DF-3486 (stock node creation panics first; verified with DF-3486's validated fix applied). Root builds the graph; traffic can be unprivileged/remote - mbuf-map exhaustion DoS. Fix validated on guest module: DOWN test -> 256->256 clusters, zero growth, ENETDOWN semantics preserved.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3541 Β· 14 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
eiface_leak.c β€” 6.0 KB view raw
build.sh β€” 173 B view raw
run.sh β€” 223 B view raw
run.log β€” 1.2 KB view raw
leak_before.txt β€” 326 B view raw
leak_after.txt β€” 327 B view raw
fix_run.log β€” 462 B view raw
unpriv_check.txt β€” 183 B view raw
env.txt β€” 1.5 KB view raw
fix.diff β€” 373 B view raw
verdict.json β€” 3.4 KB view raw
manifest.json β€” 957 B view raw

DF-3541 β€” v1 ng_eiface: mbuf leak on every packet delivered to a DOWN interface (ng_eiface_rcvdata returns ENETDOWN without freeing)

What

ng_eiface_rcvdata() (sys/netgraph/eiface/ng_eiface.c:514-515):

    if ( !(ifp->if_flags & IFF_UP) )
        return (ENETDOWN);

v1 netgraph data-delivery convention is that rcvdata consumes the mbuf on every path (see ng_frame_relay.c:415-417 bad: NG_FREE_DATA(m, meta); v1 ng_send_data passes ownership and its callers NULL their pointers). The ng7 twin frees explicitly here (NG_FREE_M(m) before the identical ENETDOWN return, sys/netgraph7/eiface/ng_eiface.c), confirming the omission is a defect, not a convention.

Every packet pushed into the ether hook while the ngethX interface is administratively DOWN (the default state after node creation) leaks one mbuf + one cluster (>MHLEN packets). Unbounded: 500 packets β†’ exactly +500 clusters (run.log); line-rate traffic to a down member of a root-built graph exhausts the kernel mbuf map (0 requests for memory denied starts climbing, network subsystem fails).

Trigger privilege

ROOT ONLY (verified: uid 1001 β†’ EPERM at v1 ngc_attach, sys/netgraph/socket/ng_socket.c:172). The graph must be built by root; traffic through it can come from any source. Severity Medium (kernel memory exhaustion DoS; needs a configured netgraph path).

Build / Run (in-guest, root)

Requires DF-3486's validated fix applied to ng_eiface.c first β€” on stock kernels v1 eiface node creation panics before this bug is reachable (that is DF-3486, already filed; this finding is the next defect behind it).

sh build.sh       # applies DF-3486 unblock, builds v1 ng_eiface.ko + trigger
sh run.sh         # loads stock v1 core/socket + rebuilt eiface; runs leak test
# DOWN (default): +500 clusters per 500 packets
# control (ifconfig ngethX up mid-run): zero growth

Fix

fix.diff: free the mbuf before returning ENETDOWN (matches the ng7 twin). Validated: same test on the fixed module β†’ zero growth, ENETDOWN semantics preserved.

VERDICT.md
↓ download raw

DF-3541 β€” VERDICT

Status: reproduced. Impact: dos (unbounded kernel heap/mbuf exhaustion). Confidence: certain.

Reproduction

Guest: DragonFly 6.5-DEVELOPMENT #0 (stock INVARIANTS kernel). v1 stack: stock /boot/kernel/netgraph.ko + ng_socket.ko, ng_eiface.ko rebuilt from /usr/src with DF-3486's validated fix (without it, node creation panics first β€” DF-3486, already filed; this finding sits behind it).

eiface_leak.c (v1 ABI: NG_VERSION 2, u_int16_t arglen, header order flags/token/typecookie/cmd; sockaddr data at offset 2; node addressed as "name:"):

  1. control socket (node named "eileak" via bind), data socket connected;
  2. NGM_MKPEER eiface on hook "ether";
  3. sleep 2 (DF-3486 deferred ether_ifattach) β†’ ngethN exists, DOWN (flags=\<BROADCAST,SIMPLEX,MULTICAST,PROXY>, no IFF_UP);
  4. 500 Γ— 2048-byte packets sent to the "ether" hook.

Result (run.log / leak_before.txt / leak_after.txt):

  • every sendto returns ENETDOWN (errno 50) β€” the ng_eiface.c:514-515 path executes;
  • mbuf clusters in use: 256 β†’ 756 (+500, exactly one per packet); "mbufs and mbuf clusters allocated to data": 268 β†’ 768 (+500).

Control test (same unfixed module): interface brought UP during the trigger's wait window β†’ clusters 756 β†’ 756, zero growth (packets consumed by ether_input). This isolates the ENETDOWN path as the leak.

Unprivileged check: uid 1001 β†’ EPERM at the control socket (unpriv_check.txt) β€” root-gated configuration, as with all v1 netgraph control paths.

Root cause (source trace)

Impact: each packet to a DOWN ngethX through its ether hook permanently leaks one mbuf (+cluster). At line rate this exhausts the mbuf map β€” kernel memory exhaustion DoS (Medium: requires a root-built netgraph path feeding the interface; the traffic itself can be unprivileged).

Exploit chain

DoS only β€” no memory-safety violation (no overwrite; pure leak).

Fix validation (fix.diff)

m_freem(m) added before the ENETDOWN return (one-line, mirrors the ng7 twin). Rebuilt module, identical test (interface DOWN, 500Γ—2048): clusters 256 β†’ 256, zero growth; ENETDOWN still returned per send (fix_run.log). fix_status: fixed.

Notes

  • The DF-3486 unblock patch is scaffolding to make the module testable on stock kernels; it is NOT part of this finding's fix (fix.diff contains only the m_freem line).
  • Meta leaked? No β€” meta was already freed earlier (NG_FREE_META at ng_eiface.c:507); only the mbuf leaks.

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Identical DOWN-interface test on the fixed module: mbuf clusters 256 -> 256 (zero growth vs +500 baseline), ENETDOWN still returned for every send; interface-UP control unchanged. Leak closed.

fix_run.log (fix_before/fix_after netstat embedded); leak_after.txt vs fix_run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (stock kernel; fix applied as a module rebuild only)

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (before/after netstat -m with +500 clusters and the per-send ENETDOWN); leak_before.txt / leak_after.txt; fix_run.log (fixed module: 256 -> 256, zero growth); unpriv_check.txt (EPERM uid 1001); env.txt

PoC changes

v1 ABI corrections: NG_VERSION 2, u_int16_t arglen, header field order flags/token/typecookie/cmd; DFly sockaddr_ng data at offset 2 (sa_family_t is __uint8_t); node naming via synchronous bind() (v1 control messages are async through netisr); data socket connect needs the trailing colon (bare name parses as a hook path with here==NULL); added a pre-send wait window for the UP-interface control test.

Verified recommended fix

m_freem(m) before the ENETDOWN return in ng_eiface_rcvdata (mirror the ng7 twin's NG_FREE_M) β€” see fix.diff.

Verdict

Reproduced on the stock guest (v1 netgraph/ng_socket from /boot/kernel, ng_eiface.ko rebuilt with DF-3486's unblock fix so the node can be created at all): 500 x 2048-byte packets delivered to the ether hook of a DOWN ngethX return ENETDOWN (errno 50) from ng_eiface.c:514-515 and leak exactly +500 mbuf clusters (256 -> 756 in use; 268 -> 768 allocated-to-data); the control run with the interface UP shows zero growth, isolating the ENETDOWN path. Root cause: ng_eiface_rcvdata returns ENETDOWN without m_freem(m) although v1 rcvdata owns the mbuf (ng_send_data hands over ownership, callers NULL their pointers) β€” the ng7 twin frees explicitly at the same spot, proving the deviation. Unbounded kernel heap exhaustion at traffic rate = DoS (Medium: root must build the netgraph path; traffic can be unprivileged). Fix validated: m_freem(m) before the return gives zero growth with ENETDOWN semantics preserved.