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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3527 Β· 18 files
FileTypeDescriptionSize
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)
VERDICT.md
↓ download raw

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

  1. kldload ng_socket; kldload ./ng_iface.ko
  2. ngctl mkpeer . iface inet inet (creates node ng0); ifconfig ng0 up β†’ flags <UP,POINT2POINT,RUNNING,...> β€” EBUSY path armed.
  3. Control: 512 Γ— NGM_IFACE_GET_IFNAME (normal error-free path) β†’ vmstat -m netgraph unchanged (2.01K/40.0M).
  4. 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).
  5. 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 by ng_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_mesg built 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.ko does 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Module-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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (same kernel; module-only swap: ng_iface.ko with DF-3485+DF-3486+this fix)

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