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)
PoC verification
Evidence pack
findings/poc/DF-3511 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:261path = 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) == NULLand!linker_api_available()(ng_socket.c:285-288) it doeserror = ENXIO; goto done; done:(ng_socket.c:351) sits after therelease:block (ng_socket.c:344-350) that freespath,control, andm.msghas 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.
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)
- 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 tongs_rcvmsgand were echoed back to the socket as datagrams β sendto appeared to "succeed"). Diagnosed withdiag.c(response dump: foreign cookie field, echoed payload). - v2 hand-rolls the ng7 kernel layout β immediately produced the expected EINVAL (control) / ENXIO (leak) split.
- Memory accounting:
vmstat -mis unreliable for kld-loaded malloc types on this guest (same observation as DF-3474) β usedvm.stats.vm.v_free_count+netstat -mmbuf 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
fixedConfirmed kernel references
- sys/netgraph7/socket/ng_socket.c:285
- sys/netgraph7/socket/ng_socket.c:287
- sys/netgraph7/socket/ng_socket.c:261
- sys/netgraph7/socket/ng_socket.c:246
- sys/netgraph7/socket/ng_socket.c:344
- sys/netgraph7/socket/ng_socket.c:351
- sys/netgraph7/socket/ng_socket.c:294
- sys/netgraph7/socket/ng_socket.c:182
- sys/netgraph7/socket/ng_socket.c:1175
Detail
Exploit chain
root -> socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL) -> sendto(NGM_MKPEER, type=
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
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.
No comments yet.