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

ngc_send MKPEER autoload: `goto done` bypasses every free β€” msg + path + mbuf leaked per sendto; module autoload via AF_NETGRAPH always ENXIO

Summary

ngc_send intercepts NGM_MKPEER naming an unloaded type. When linker_api_available() is 0 it does 'error = ENXIO; goto done;' - done: sits AFTER the release: block that frees path/control/m, and msg has no free on this path at all. On DragonFly pru_send handlers run on a protocol thread with curproc==NULL, so linker_api_available() returns 0 on EVERY send - the leak path is the only outcome of MKPEER-with-unloaded-type, and each sendto permanently leaks the ng_mesg buffer (user-chosen size), the path buffer and the datagram mbuf. REPRODUCED on guest: 50k bogus-type sends -> 50k x ENXIO, 33.5MB leaked, mbufs-in-use up by exactly one per send (12 -> 50012); control phase flat. Fix validated (free msg + goto release): identical behavior, zero growth. Root-gated; ceiling unbounded kernel heap + mbuf-pool exhaustion. Corollary: kld-based node-type autoload through ng sockets is functionally broken (always ENXIO).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3511 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 4.7 KB ↓ raw
trigger.c β€” 5.0 KB view raw
diag.c β€” 3.0 KB view raw
build.sh β€” 239 B view raw
run.sh β€” 295 B view raw
build.log β€” 11.7 KB view raw
run.log β€” 917 B view raw
fix_run.log β€” 770 B view raw
env.txt β€” 190 B view raw
fix.diff β€” 490 B view raw
manifest.json β€” 1.9 KB view raw
verdict.json β€” 5.4 KB view raw

DF-3511 β€” ng_socket.c ngc_send() MKPEER module-autoload path: goto done

skips every free β€” msg + path + mbuf leaked per sendto (and module autoload

via AF_NETGRAPH is always ENXIO)

Where

sys/netgraph7/socket/ng_socket.c, ngc_send():

  • msg = kmalloc(len + 1, M_NETGRAPH_MSG, M_WAITOK) at ng_socket.c:261
  • path = kmalloc(len + 1, M_NETGRAPH_PATH, M_WAITOK) at ng_socket.c:246
  • MKPEER autoload interception at ng_socket.c:276-307: when ng_findtype(mkp->type) == NULL and !linker_api_available() (ng_socket.c:285-288) it does error = ENXIO; goto done;
  • done: (ng_socket.c:351) sits after the release: block (ng_socket.c:344-350) that frees path, control, and m. msg has no other free on this path (it is only kfree'd at :265/:295/:301, all other exits of the loader block).

On DragonFly, pru_send handlers execute on a socket protocol thread where curproc == NULL, so linker_api_available() (ng_socket.c:1175-1188: curproc == NULL || namei_oc == NULL) returns 0 on every send β€” the goto done path is not a boot-time corner case, it is the only outcome of "MKPEER with a type that is not already loaded":

  • every such sendto(2) permanently leaks kmalloc(len+1, M_NETGRAPH_MSG) (user-chosen len), the path allocation, and the sendto mbuf;
  • module autoload through AF_NETGRAPH sockets never works (always ENXIO).

Reachability / privilege

Control sockets are privilege-gated (ngc_attach, ng_socket.c:182-184: caps_priv_check(SYSCAP_RESTRICTEDROOT)) β†’ the trigger requires root. Ceiling: unbounded kernel-memory + mbuf leak (resource-exhaustion DoS) by an already-privileged user; no memory corruption, no unprivileged route.

Reproduce (in guest, root)

sh /root/df3511/build.sh          # builds netgraph7 netgraph.ko + ng_socket.ko + trigger
sh /root/df3511/run.sh            # kldloads modules, runs A (control) then B (leak)

Expected (unfixed kernel β€” see run.log):

[type=socket  ] sendto ok=0 fail=50000 ... errno 22 (EINVAL): 50000   <- control
[type=df3511x ] sendto ok=0 fail=50000 ... errno 6  (ENXIO):  50000   <- leak live
free-before: 839636 -> afterA: 839220 (flat) -> afterB: 830848 (-8372 pages)
mbufs in use: 14 -> 12 (A) -> 50012 (B)     # exactly +1 leaked mbuf per send

With fix.diff applied to the guest's /usr/src copy and the module rebuilt (fix_run.log): identical ENXIO/EINVAL behavior but memory flat and mbufs 7->7.

Fix

fix.diff (2 hunks): free msg and goto release (the pattern already used by the sibling error exits at ng_socket.c:294-297), and drop the now-unused done: label.

VERDICT.md
↓ download raw

DF-3511 β€” VERDICT

Status: reproduced (leak/DoS, root-gated) Β· fix validated in-guest

What was claimed

ngc_send() intercepts NGM_MKPEER whose type is not loaded (ng_socket.c:276-307). If linker_api_available() returns 0 it takes error = ENXIO; goto done; (ng_socket.c:285-288). done: (ng_socket.c:351) is after the release: block (ng_socket.c:344-350) that frees path, control and m, and msg (kmalloc'd at ng_socket.c:261) has no free on this path at all. Every sendto(2) that hits the branch leaks three allocations: the ng_mesg buffer (user-chosen len+1 bytes), the path buffer, and the datagram mbuf.

How it was proven

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC), netgraph7 built from /usr/src as modules (stock /boot modules are the old sys/netgraph stack). Trigger: root opens an AF_NETGRAPH NG_CONTROL socket and sends a correctly laid-out (netgraph7 header) NGM_MKPEER addressed to its own node ("."), 50k iterations per phase:

  • Phase A (control) β€” type "socket" (loaded): the loader block is skipped, generic MKPEER fails in ngs_constructor. Result: 50,000x EINVAL, free pages 839636 -> 839220 (background noise), mbufs in use 14 -> 12. No leak.
  • Phase B β€” type "df3511x" (never loaded): ng_findtype() returns NULL, linker_api_available() returns 0 (see below) -> ENXIO + goto done. Result: 50,000x ENXIO, free pages 839220 -> 830848 (-8,372 pages β‰ˆ 33.5 MB, ~683 bytes/send), mbufs in use 12 -> 50,012 β€” exactly one leaked mbuf per sendto. run.log, run.2.log-equivalent numbers inline.

The 1:1 mbuf fingerprint (50000 sends -> +50000 mbufs, current/max counter) removes any doubt that the growth is coincidental.

Why linker_api_available() is always 0 here

linker_api_available() (ng_socket.c:1175-1188) returns 0 when curproc == NULL. On DragonFly, pru_send handlers run on the socket's protocol (lwkt) thread, which has no process context β€” so every MKPEER naming an unloaded type takes the leak path. This also means module autoload through AF_NETGRAPH control sockets is functionally broken (always ENXIO) on this kernel β€” corroborating that this branch had effectively never been exercised with a correct netgraph7 message before.

PoC iteration notes (what had to be fixed)

  1. v1 used the stock userland <netgraph/ng_message.h> (old sys/netgraph layout, NG_VERSION 2β†’overridden 8). The old header layout does NOT match netgraph7 (ng7: arglen@+4, cmd@+8, flags@+12, token@+16, typecookie@+20, cmdstr[32] -> 56-byte header): the kernel silently mis-parsed v1 messages (they fell through to ngs_rcvmsg and were echoed back to the socket as datagrams β€” sendto appeared to "succeed"). Diagnosed with diag.c (response dump: foreign cookie field, echoed payload).
  2. v2 hand-rolls the ng7 kernel layout β€” immediately produced the expected EINVAL (control) / ENXIO (leak) split.
  3. Memory accounting: vmstat -m is unreliable for kld-loaded malloc types on this guest (same observation as DF-3474) β€” used vm.stats.vm.v_free_count + netstat -m mbuf counters instead.

Fix validation

fix.diff (authored against the repo's read-only sys/ tree, applied to the guest's /usr/src copy only):

  • hunk 1: kfree(msg, M_NETGRAPH_MSG); error = ENXIO; goto release; (mirrors the sibling exits at ng_socket.c:294-297);
  • hunk 2: remove the now-unused done: label (-Werror=unused-label).

Rebuilt ng_socket.ko in-guest, rebooted (ng_socket.ko cannot be kldunloaded: ngs_mod_event MOD_UNLOAD returns EBUSY), kldloaded the patched module and re-ran the identical workload (fix_run.log):

  • phase A: 50,000x EINVAL (unchanged), free 870303 -> 869306;
  • phase B: 50,000x ENXIO (behavior preserved), free 869306 -> 869202 (flat vs -8,372 baseline), mbufs in use 7 -> 7 (baseline: +50,000).

Bad behavior gone; no functional regression visible from the socket API.

Impact ceiling

Root-gated (control socket requires caps_priv_check(SYSCAP_RESTRICTEDROOT), ng_socket.c:182-184). Unbounded kernel heap + mbuf leak per sendto β†’ memory-exhaustion DoS by a privileged user (no unprivileged trigger, no memory corruption, no info disclosure). The identical ENXIO side-effect shows module autoload via ng sockets is broken, which is a functional bug in its own right. Severity: Low.

Guest was reset to the with-src snapshot after validation; /usr/src in the repo tree was never modified.

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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (netgraph7 ng_socket.ko rebuilt in-guest with fix.diff, kldloaded after reboot)

Confirmed kernel references

Detail

Exploit chain

root -> socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL) -> sendto(NGM_MKPEER, type=) -> ngc_send: ng_findtype()==NULL, linker_api_available()==0 -> ENXIO + goto done -> msg + path + mbuf leaked per send -> repeat: unbounded kernel heap + mbuf-pool exhaustion (mbuf max 146632 on this guest -> ~100k sends caps all further mbuf allocation system-wide; kmem likewise drains). Stop at exhaustion = permanent local DoS; requires root throughout.

Evidence (decisive lines)

run.log - baseline A/B: A 50000x EINVAL free 839636->839220 mbufs 14->12; B 50000x ENXIO free 839220->830848 (-8372 pages), mbufs 12->50012 (exactly +1/send); fix_run.log - patched module, same workload: B still 50000x ENXIO but free 869306->869202 (flat) and mbufs 7->7; trigger.c - ng7-layout MKPEER trigger; diag.c - header-layout mis-parse discovery tool; VERDICT.md - full narrative incl. why curproc==NULL makes the branch always-taken; fix.diff - 2-hunk validated fix

PoC changes

v1 used the stock userland (old sys/netgraph layout); netgraph7 uses a different 56-byte header (arglen@+4, cmd@+8, flags@+12, token@+16, typecookie@+20, cmdstr[32]@+24), so v1 messages were silently mis-parsed (echoed back by ngs_rcvmsg, sendto appeared to succeed). v2 hand-rolls the ng7 kernel layout; the ENXIO/EINVAL split appeared immediately. Memory measurement switched from vmstat -m (unreliable for kld-loaded malloc types on this guest, per DF-3474) to vm.stats.vm.v_free_count + netstat -m mbuf counters. Fix needed a second hunk (drop the done: label) because -Werror=unused-label breaks the module build otherwise.

Verified recommended fix

In ngc_send()'s MKPEER loader block, free msg and take the release path when the linker API is unavailable (kfree(msg, M_NETGRAPH_MSG); error = ENXIO; goto release;) and delete the then-unused done: label - see fix.diff.

Verdict

ngc_send()'s NGM_MKPEER autoload interception (ng_socket.c:276-307) leaks three allocations whenever linker_api_available() returns 0: it jumps to done: (ng_socket.c:287->351), which is past the release: block that frees path/control/m (ng_socket.c:344-350), and msg (kmalloc(len+1, M_NETGRAPH_MSG), ng_socket.c:261) is never freed on this path. On DragonFly pru_send handlers run on a protocol thread with curproc==NULL, so linker_api_available() (ng_socket.c:1175-1188) returns 0 on EVERY send - the branch is the sole outcome of MKPEER-with-unloaded-type, not a boot-time corner case. Reproduced in-guest with a netgraph7-layout trigger: 50k bogus-type sendto -> 50000x ENXIO, 33.5MB (8372 pages) of kernel memory permanently leaked (~683 B/send), and netstat -m mbufs-in-use grew by EXACTLY one per send (12 -> 50012) - a 1:1 fingerprint; control phase (loaded type, EINVAL) was flat. Corollary functional bug: module autoload through AF_NETGRAPH sockets never works (always ENXIO). Root-gated (control socket: caps_priv_check(SYSCAP_RESTRICTEDROOT), ng_socket.c:182-184), so ceiling is resource-exhaustion DoS by a privileged user - no unprivileged route, no memory corruption, no disclosure. fix.diff (kfree(msg)+goto release, mirroring the sibling exits at :294-297, plus removal of the now-unused done: label) validated in-guest: identical EINVAL/ENXIO behavior with flat memory and mbufs 7->7 across the same 100k-send workload.