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)
PoC verification
Evidence pack
findings/poc/DF-3541 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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:"):
- control socket (node named "eileak" via bind), data socket connected;
- NGM_MKPEER eiface on hook "ether";
- sleep 2 (DF-3486 deferred ether_ifattach) β ngethN exists, DOWN (flags=\<BROADCAST,SIMPLEX,MULTICAST,PROXY>, no IFF_UP);
- 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)
- sys/netgraph/eiface/ng_eiface.c:514-515 β
return (ENETDOWN)with nom_freem(m); - ownership passed by the framework: v1 ng_send_data
(sys/netgraph/netgraph/ng_base.c:1684-1687) hands
mto rcvdata and returns; NG_SEND_DATA callers NULL their mbuf pointer (sys/netgraph/netgraph.h:224-229), so nobody else can free it; - sibling v1 nodes free on this path (ng_frame_relay.c:415-417) and the ng7 twin frees explicitly (NG_FREE_M before the same ENETDOWN return, sys/netgraph7/eiface/ng_eiface.c rcvdata) β deviation, not convention.
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
fixedIdentical 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
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.
No comments yet.