Error-path kernel heap leaks in v1 ng_parse: ng_get_string_token leaks the token buffer on unterminated quote and ng_string_unparse leaks the ng_encode_string buffer on append-ERANGE (both already fixed in the ng7 copy)
Summary
ng_get_string_token() allocates cbuf and returns NULL on no-closing-quote without kfree (ng7 added the kfree). ng_string_unparse() allocates s=ng_encode_string(raw) and the append returns ERANGE without freeing s; ng_unparse_composite()'s mid-loop appends similarly leak the 20KB workBuf. NGM_ASCII2BINARY with an unterminated quote leaks ~2x strlen per message; NGM_BINARY2ASCII whose cmdstr field lands on attacker bytes (strlen 19980) leaks the ~80KB encode buffer per call. VERIFIED on guest (root, kldload ng_socket): 2000x unterminated-quote -> 16-17MB; 1500x BINARY2ASCII with inner arglen=20000 -> 117-118MB (~78KB/call); controls flat; instrumented module trace shows enc+ without enc- per call. Kernel heap exhaustion, attacker-chosen per-call size up to ~256KB, unbounded repeat; ngc attach requires real root. The ng7 file contains the fixes on the corresponding paths - the live stack was never caught up. Fix validated (port the ng7 frees): 1MB/0MB residual vs 17MB/118MB baseline.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3494 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ngpleak.c | β | 7.3 KB | view raw | |
| build.sh | β | 81 B | view raw | |
| run.sh | β | 126 B | view raw | |
| build.log | β | 63 B | view raw | |
| run.log | β | 423 B | view raw | |
| run.2.log | β | 423 B | view raw | |
| run.3.log | β | 554 B | view raw | |
| fixrun.log | β | 552 B | view raw | |
| dmesg.txt | β | 1.3 KB | view raw | |
| fix.diff | β | 2.3 KB | view raw | |
| VERDICT.md | β | 2.7 KB | β raw | |
| README.md | β | 3.0 KB | β raw | |
| env.txt | β | 247 B | view raw |
DF-3494 β v1 ng_parse error-path kernel heap leaks (both fixed in ng7)
File: sys/netgraph/netgraph/ng_parse.c (live stack, stock netgraph.ko)
The two sites
-
ng_get_string_token()(:1548-1625) βcbuf = kmalloc(strlen(s + start))(:1559); every failure-to-find-a-closing-quote path returns NULL at :1624 without freeing cbuf. ng7 added the missingkfree(cbuf)on the same path (ng7 ng_parse.c "no closing quote"). Reachable with attacker-sized strings:NGM_ASCII2BINARYof{name="AAAAβ¦(no closing quote) leaks ~strlen bytes per tokenization β and the composite parse path tokenizes the same unterminated quote 2-3Γ per message (skip_value + field loop + field parse). -
ng_string_unparse()(:722-735) β allocatess = ng_encode_string(raw)and theNG_PARSE_APPEND("%s", s)macro doesreturn (ERANGE)on cbuf exhaustion without freeing s (:731). ng7 restructured to freeson the append failure. BecauseNGM_BINARY2ASCIIunparses the arguments buffer with theng_parse_ng_mesg_type(whose "cmdstr" field lands on attacker-chosen bytes), a payload whose string encoding exceeds the 20 KB response cbuf leaks the whole encode buffer (~4Γ input size). Additionallyng_unparse_composite()'s own mid-loop appends (" ","%d=","%s=":1274-1285) return ERANGE without freeing the 20 KBworkBufβ same class, fixed by ng7'sgoto failrestructure (covered by fix.diff as defense-in-depth).
Both are kernel-heap exhaustion primitives: unbounded repeat, attacker chosen per-call size (up to ~64 KB string / ~256 KB encode).
Reproduce (guest, root)
kldload ng_socket
cc -O -o ngpleak ngpleak.c && ./ngpleak 2000 1500 5000
- (a) 2000 Γ ASCII2BINARY unterminated quote β 16-17 MB gone
(
vm.stats.vm.v_free_count), EINVAL is the expected sync result. - (b) 1500 Γ BINARY2ASCII whose inner
arglen=20000/0xAA payload makes the string encode exceed the 20 KB cbuf β 117-118 MB gone (~78 KB/call = the ~80 KB encode buffer), ERANGE is the expected sync result. - Differential controls (
nosuchnode:path = transport only β no leak;dlen=2000success path β no leak) pin the leak to the ERANGE path. dmesg.txtshows the kprintf-instrumented proof: per callenc+ 0xfffff801β¦(β80 KB) with noenc-β the encode buffer is never freed.
Fix validation
fix.diff ports both ng7 fixes (free cbuf at :1624; free s on
append failure in ng_string_unparse) plus the workBuf fail: label.
Module rebuilt in-guest and reloaded: (a) 1 MB (noise), (b)
0 MB / 41 pages (noise); vmstat -m shows M_NETGRAPH fully
reclaimed (0 in use).
Privilege gate
ngc socket attach requires SYSCAP_RESTRICTEDROOT
(sys/netgraph/socket/ng_socket.c:172-173) β real root (jailed root
lacks it). Severity accordingly Low; value = upstream parity with the
already-fixed ng7 code and defense-in-depth for jails/containers that
do grant netgraph access.
DF-3494 β VERDICT
reproduced / dos (kernel heap exhaustion) / certain β two error-path kernel heap leaks in the live v1 ng_parse, both of which the ng7 copy of the same file already fixed upstream.
What was reproduced (stock netgraph.ko, root)
- (a) unterminated-quote token leak β
NGM_ASCII2BINARY{name="+ 5000 'A's, 2000 iterations:v_free_countdropped 4239-9749 pages (16-38 MB) across three runs (~2 tokenizations of the ~5 KB string leak per message: skip_value + field loop). - (b) string-unparse encode leak β
NGM_BINARY2ASCIIwhose inner message makes the ng_parse_ng_mesg_type "cmdstr" field land on attacker bytes withstrlen=19980(seedmesg.txtinstrumentation:enc+with noenc-): 1500 iterations consumed 117-118 MB (~78 KB/call = the ~80 KB ng_encode_string buffer). Sync error ERANGE is the leak-path signature. - Controls: same-size sends to a nonexistent node (transport only) β no
leak;
dlen=2000unparse-success path β no leak. The leak is exclusively on the error paths, matching the code.
Root causes (line-cited)
ng_get_string_token:1559 allocates cbuf, :1624 returns NULL on no-closing-quote without kfree β leak per failed tokenization.ng_string_unparse:727NG_PARSE_APPEND("%s", s)macro-returns ERANGE at :731 without freeing the ng_encode_string buffers. (Plus the defense-in-depth member:ng_unparse_composite's mid-loop appends :1274/:1278/:1285 return ERANGE leaking the 20 KB workBuf.)
ng7's copy of ng_parse.c contains the frees for both (kfree before "no closing quote" return; ng_parse_append + kfree(s) on append error; goto-fail for workBuf) β this finding is catching the live stack up to the already-fixed ng7 code.
Fix validation
fix.diff (v2) applied to the guest's /usr/src copy, netgraph.ko
rebuilt and reloaded: (a) 300 pages (~1 MB, noise) and (b)
41 pages (0 MB) β both leaks eliminated; vmstat -m shows
M_NETGRAPH 0 in use. Baseline vs patched outputs in run.3.log vs
fixrun.log.
Impact ceiling
Kernel heap exhaustion (system-wide memory starvation β allocation
failures/panic under pressure) by the netgraph-controlling user. The
ngc socket attach gate is SYSCAP_RESTRICTEDROOT
(ng_socket.c:172-173), i.e. real root β hence severity Low, filed for
upstream parity and defense-in-depth.
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
fixedfix.diff (v2) applied to guest /usr/src copy, netgraph.ko rebuilt and reloaded after reboot: (a) 300 pages (~1 MB, noise) vs baseline 16-17 MB; (b) 41 pages (0 MB) vs baseline 117-118 MB; vmstat -m shows M_NETGRAPH fully reclaimed. Both leaks eliminated; no new failures.
fixrun.log vs run.3.log; vmstat -m before/after in transcript
Confirmed kernel references
Detail
Exploit chain
root netgraph user (or any context granted netgraph control) repeats malformed ASCII2BINARY / oversized-string BINARY2ASCII messages -> unbounded kernel heap consumption (attacker-chosen per-call size up to ~256 KB) -> memory starvation / allocation-failure panics system-wide
Evidence (decisive lines)
run.3.log (baseline: (a) 17 MB, (b) 118 MB); run.log/run.2.log (additional baseline runs); fixrun.log (patched: 1 MB / 0 MB); dmesg.txt (instrumented alloc/free trace: 'DBG enc+ 0xfffff801183c0000' with no 'enc-')
PoC changes
Harness written fresh against the v1 ngc wire protocol (sendto ng_mesg bytes to path '.'); initial assumptions fixed: EINVAL/ERANGE sync results are the EXPECTED leak-path signatures, not failures; (b) payload sized 20 KB to fit the socket buffer; workBuf hypothesis corrected to the encode-buffer sink after instrumented-module trace (dmesg.txt).
Verified recommended fix
Port the ng7 frees: kfree(cbuf) before the no-closing-quote return; free the encode buffer when the append fails; fail-label the composite workBuf appends (see fix.diff)
Verdict
Two error-path kernel heap leaks in the live v1 ng_parse, both already fixed in the ng7 copy of the same file: (1) ng_get_string_token() kmallocs cbuf (:1559) and returns NULL on an unterminated quote without freeing it (:1624) β leaked ~2x per malformed NGM_ASCII2BINARY message (skip_value + field-loop tokenizations), measured 16-38 MB over 2000-4000 calls; (2) ng_string_unparse() allocates the ng_encode_string buffer and NG_PARSE_APPEND("%s", s) returns ERANGE without freeing it (:727-731) β reached via NGM_BINARY2ASCII with the ng_parse_ng_mesg_type cmdstr field landing on attacker bytes (strlen 19980 -> ~80 KB alloc), measured 117-118 MB over 1500 calls (~78 KB/call). kprintf instrumentation (dmesg.txt) shows enc+ with no enc- per call. Differential controls (nonexistent-path transport sends, unparse-success payloads) show no leak, pinning both to the error paths. Root-gated (SYSCAP_RESTRICTEDROOT, ng_socket.c:172).
No comments yet.