ng_iface (v1): NGM_IFACE_POINT2POINT/NGM_IFACE_BROADCAST early 'return (EBUSY)' leaks the control message β unbounded, persistent M_NETGRAPH kernel-heap leak
Summary
ng_iface_rcvmsg() rejects POINT2POINT/BROADCAST with a direct 'return (EBUSY)' when the interface is IFF_UP, bypassing the kfree(msg) at the function tail. Under the v1 netgraph contract the receiving node's rcvmsg owns the message and the sender allocates kmalloc(len+1, M_NETGRAPH) per sendto() and never frees it. Every such message while the ng iface is UP leaks sizeof(struct ng_mesg)+arglen+1 bytes. VERIFIED live: 2048 sends -> +2048 allocations/+40MB netgraph heap per run, accumulating linearly (40M->80M->120M) and never returning; error-free control path shows zero growth; fix validated by module-swap A/B (leak gone, EBUSY semantics identical). Root-gated (control socket SYSCAP_RESTRICTEDROOT); unbounded kernel-heap leak repeatable to kmem exhaustion (allocator limit 390M tested). Note: DF-3486 residual on THIS file re-verified live (with only DF-3485's fix applied, creation panics at ng_iface.c:605 'try holding ifnet lock in netisr' - covered by DF-3486's finding text).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3527 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 4.6 KB | β raw | |
| leak.c | β | 4.3 KB | view raw | |
| leak_trigger.sh | β | 1.4 KB | view raw | |
| build.sh | β | 286 B | view raw | |
| run.sh | β | 157 B | view raw | |
| build.log | β | 73 B | view raw | |
| run.log | β | 1.4 KB | view raw | |
| run.baseline.log | β | 1.6 KB | view raw | |
| run.2.log | β | 714 B | view raw | |
| fix.diff | β | 523 B | view raw | |
| fix_build.log | β | 1.2 KB | view raw | |
| fix_run.log | β | 1.5 KB | view raw | |
| panic.iface_residual_df3486.txt | β | 696 B | view raw | |
| mppc_loadcheck.log | β | 210 B | view raw | |
| env.txt | β | 691 B | view raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 4.6 KB | view raw |
DF-3527 β ng_iface (v1) NGM_IFACE_POINT2POINT/BROADCAST early return (EBUSY) leaks the control message
What
ng_iface_rcvmsg() (sys/netgraph/iface/ng_iface.c:660-680) rejects
NGM_IFACE_POINT2POINT / NGM_IFACE_BROADCAST with a direct
return (EBUSY); (line 666) when the interface is IFF_UP, bypassing the
kfree(msg, M_NETGRAPH) at the function tail (line 732). Under the v1
netgraph message contract the receiving node's rcvmsg() owns the message
(sys/netgraph/netgraph/ng_base.c:1176-1200, "It is up to the message
handler to free the message"); the sender (ngc_send,
sys/netgraph/socket/ng_socket.c:246-258) kmallocs len + 1 bytes per
sendto() and never frees them itself (comment at :256: "The callee will
free the xmsg when done"). Every such message therefore leaks
sizeof(struct ng_mesg) + arglen + 1 bytes of M_NETGRAPH kernel heap.
Impact
Unbounded kernel-heap leak, repeatable to kmem exhaustion (DoS), by any
holder of a netgraph control socket. Control sockets require
caps_priv_check(SYSCAP_RESTRICTEDROOT) (ng_socket.c:172-176), i.e. root β
hence severity Low, same framing as DF-0607/DF-3485/DF-3486.
Build / run (evidence in this pack)
The stock guest panics before an ng_iface node can exist (DF-3485, then DF-3486 at ng_iface.c:605), so the PoC runs against a guest whose /usr/src carries only the two already-validated fixes for those findings:
# in guest: cc -O2 -o /tmp/df3527_leak /tmp/df3527_leak.c # (build.sh) ngctl mkpeer . iface inet inet # create ng0 (patched modules) ifconfig ng0 up # arm the EBUSY path /tmp/df3527_leak ng0 512 16384 1 # control: GET_IFNAME, no leak /tmp/df3527_leak ng0 2048 16384 2 # leak: POINT2POINT on UP ng0 vmstat -m | grep -i netgraph # before/after delta ~32MB
Expected (leak/baseline kernel): all 2048 sends return EBUSY and
vmstat -m shows ~2048 * (52 + 16384 + 1) β 32 MB of M_NETGRAPH growth;
the control run shows ~0 growth.
Expected (fix-applied kernel): identical sends, ~0 growth.
Files
leak.c PoC source (v1 ng_mesg built locally, no headers needed) leak_trigger.sh guest-side driver incl. control run + vmstat deltas build.sh exact build command run.sh host-side wrapper fix.diff the one-hunk fix (error = EBUSY + goto done) VERDICT.md full narrative incl. DF-3486 residual-state verification verdict.json machine verdict (persist_poc.py schema)
DF-3527 β VERDICT
Status: REPRODUCED (leak, certain). Fix VALIDATED (fixed).
Finding: ng_iface_rcvmsg() returns EBUSY early (sys/netgraph/iface/ng_iface.c:666)
when the interface is IFF_UP, skipping the kfree(msg, M_NETGRAPH) at the
function tail (:732). Under the v1 netgraph contract the receiving node owns the
message ("It is up to the message handler to free the message",
sys/netgraph/netgraph/ng_base.c:1176-1200); the sender allocates
kmalloc(len + 1, M_NETGRAPH) per sendto()
(sys/netgraph/socket/ng_socket.c:246-258, comment at :256: "The callee will free
the xmsg when done"). Each NGM_IFACE_POINT2POINT/NGM_IFACE_BROADCAST message
sent while the ng iface is UP leaks sizeof(struct ng_mesg) + arglen + 1 bytes.
How it was reproduced
The stock guest cannot create any v1 ng_iface node at all (DF-3485 kfree(NULL)
panic in ng_iface_get_unit, then DF-3486 if_attach-from-netisr panic β the
latter re-verified live for this pass at ng_iface.c:605, see
panic.iface_residual_df3486.txt: if_attach() <- ng_iface_constructor+0x22d <-
ng_mkpeer() <- ng_generic_msg(), "panic: try holding ifnet lock in netisr").
The PoC therefore runs with ONLY the two already-validated fixes for those
known findings applied to the guest's /usr/src (module-only build):
DF-3485/fix.diff + DF-3486/fix.diff. Neither touches ng_iface_rcvmsg.
Steps (all as root; PF_NETGRAPH control sockets require
caps_priv_check(SYSCAP_RESTRICTEDROOT), ng_socket.c:172-176):
kldload ng_socket; kldload ./ng_iface.kongctl mkpeer . iface inet inet(creates node ng0);ifconfig ng0 upβ flags<UP,POINT2POINT,RUNNING,...>β EBUSY path armed.- Control: 512 Γ
NGM_IFACE_GET_IFNAME(normal error-free path) β vmstat -m netgraph unchanged (2.01K/40.0M). - Leak: 2048 Γ
NGM_IFACE_POINT2POINT, arglen=16384, on UP ng0 β all 2048 sendto()s return EBUSY; vmstat -m netgraph 2.01K/40.0M β 4.01K/80.0M (+2048 allocations β +40MB for a 33.6MB payload β allocator granularity). - Repeat runs accumulate linearly and never return: 40M β 80M β 120M (run.log, run.2.log). kmem limit for the allocator is 390M β ~10 runs of this size exhaust it: unbounded kernel-heap leak / DoS.
Fix validation
fix.diff (one hunk + label): error = EBUSY; goto done; instead of the
early return, so the epilogue frees the message (and resp handling is
unchanged; resp is NULL on that path).
- baseline module: 2048ΓEBUSY β +2048 allocs / +40MB, persistent.
- fixed module (same kernel, module swap only): 2048ΓEBUSY β +0 allocs / +0 bytes (6.01K/120M flat across control AND leak runs; the standing 120MB is the earlier baseline leak, unrecoverable as expected).
- semantics preserved: messages still rejected with EBUSY while UP. See fix_build.log / fix_run.log.
PoC changes vs the seed sketch
- Node address must be the ABSOLUTE form
"ng0:"β a bare"ng0"is parsed byng_path_parse()(ng_base.c) as a relative hook path on the sender's own socket node β ENOENT (first attempt failed with this). - v1
struct ng_mesgbuilt locally: header.arglen is u_int16_t in v1 (not u_int32_t as in ng7), NG_VERSION=2, NG_CONTROL=2, SOCK_DGRAM. - Drain the synchronous response after each successful send (else the socket receive buffer fills β ENOBUFS on response-carrying commands).
- vmstat -m parsing: DFly columns are
type InUse MemUse ...with K/M suffixes.
Environmental notes
- Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), cc 8.3.
- The ng_iface module used for the leak run carries DF-3485+DF-3486 fixes only; those are prerequisites for the node to exist at all and are unrelated to rcvmsg.
- Side-check recorded in mppc_loadcheck.log: stock
ng_mppc.kodoes NOT kldload on this kernel ("interface rc4.1 already present in the KLD 'kernel'" β the module Makefile statically links crypto/rc4/rc4.c while the kernel also compiles it, sys/conf/files:2268). Packaging tangle, not a memory-safety issue; documented for the DF mppc clean verdict.
Bottom line
Real, unbounded, persistent M_NETGRAPH kernel-heap leak; root-gated (netgraph control socket), hence Low severity β but trivially repeatable to kmem exhaustion. One-line-class fix validated live: leak gone, behavior identical.
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
fixedModule-swap A/B on the same kernel: baseline module leaks +2048 allocs/+40MB per 2048-message run (persistent, accumulating); module with fix.diff returns identical EBUSY semantics with ZERO allocator growth (6.01K/120M flat across control and leak runs). Leak eliminated; no behavior change.
fix_build.log (patch applied hunk#1@758 hunk#2@823, module relinked), fix_run.log (EBUSY=2048, InUse/MemUse unchanged vs control), run.log+run.2.log (baseline accumulation 40M->80M->120M)
Confirmed kernel references
Detail
Exploit chain
root (or any future unprivileged netgraph control path): socket(PF_NETGRAPH,SOCK_DGRAM,NG_CONTROL) -> sendto('ng0:', ng_mesg{NGM_IFACE_COOKIE, NGM_IFACE_POINT2POINT, arglen=N}) repeated while ng0 is UP -> each send leaks 52+arglen+1 bytes -> ~8K messages with 64K arglen exhaust the 390M kmem limit -> kernel memory starvation DoS. No memory corruption: pure allocation leak.
Evidence (decisive lines)
run.log (control 0-growth vs leak +2048 allocs/+40MB, EBUSY=2048/2048), run.2.log (linear accumulation 80M->120M), fix_run.log (fixed module: EBUSY=2048/2048 but 6.01K/120M FLAT), fix_build.log, panic.iface_residual_df3486.txt (DF-3486 residual at ng_iface.c:605 verified), env.txt, leak.c, leak_trigger.sh, fix.diff
PoC changes
node address must be absolute 'ng0:' (bare name = relative hook path -> ENOENT); v1 ng_mesg built locally (arglen is u_int16_t in v1, NG_VERSION=2, NG_CONTROL=2, SOCK_DGRAM); drain synchronous response after each success (else socket rcv buf fills -> ENOBUFS); vmstat -m parsed for InUse/MemUse columns
Verified recommended fix
Replace the early 'return (EBUSY)' in ng_iface_rcvmsg with 'error = EBUSY; goto done;' so the function epilogue frees the message (fix.diff).
Verdict
ng_iface_rcvmsg() returns EBUSY early at ng_iface.c:666 (interface IFF_UP) skipping the kfree(msg) at :732; the v1 netgraph contract makes rcvmsg the owner of the message (ng_base.c:1178, CALL_MSG_HANDLER :1186-1200) and the sender never frees it (ng_socket.c:253-258 'The callee will free the xmsg when done'), so every NGM_IFACE_POINT2POINT/NGM_IFACE_BROADCAST sendto() leaks sizeof(ng_mesg)+arglen+1 bytes of M_NETGRAPH heap. Reproduced live on the guest: 2048 EBUSY replies -> netgraph allocator 2.01K/40.0M -> 4.01K/80.0M (exactly +2048 allocations), accumulating linearly 40M->80M->120M across runs and never returning (kmem limit 390M); control run on the error-free GET_IFNAME path shows zero growth. Root-gated (control socket requires SYSCAP_RESTRICTEDROOT, ng_socket.c:172-176) hence Low severity; unbounded repeatable kernel-memory-exhaustion DoS. PoC required DF-3485+DF-3486 validated fixes in-guest only so the ng_iface node can be created at all (stock panics at ng_iface.c:605 - re-verified live this session, panic.iface_residual_df3486.txt).
No comments yet.