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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3494 Β· 13 files
FileTypeDescriptionSize
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

  1. 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 missing kfree(cbuf) on the same path (ng7 ng_parse.c "no closing quote"). Reachable with attacker-sized strings: NGM_ASCII2BINARY of {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).

  2. ng_string_unparse() (:722-735) β€” allocates s = ng_encode_string(raw) and the NG_PARSE_APPEND("%s", s) macro does return (ERANGE) on cbuf exhaustion without freeing s (:731). ng7 restructured to free s on the append failure. Because NGM_BINARY2ASCII unparses the arguments buffer with the ng_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). Additionally ng_unparse_composite()'s own mid-loop appends (" ", "%d=", "%s=" :1274-1285) return ERANGE without freeing the 20 KB workBuf β€” same class, fixed by ng7's goto fail restructure (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=2000 success path β†’ no leak) pin the leak to the ERANGE path.
  • dmesg.txt shows the kprintf-instrumented proof: per call enc+ 0xfffff801… (β‰ˆ80 KB) with no enc- β€” 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.

VERDICT.md
↓ download raw

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_count dropped 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_BINARY2ASCII whose inner message makes the ng_parse_ng_mesg_type "cmdstr" field land on attacker bytes with strlen=19980 (see dmesg.txt instrumentation: enc+ with no enc-): 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=2000 unparse-success path β†’ no leak. The leak is exclusively on the error paths, matching the code.

Root causes (line-cited)

  1. ng_get_string_token :1559 allocates cbuf, :1624 returns NULL on no-closing-quote without kfree β†’ leak per failed tokenization.
  2. ng_string_unparse :727 NG_PARSE_APPEND("%s", s) macro-returns ERANGE at :731 without freeing the ng_encode_string buffer s. (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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 + rebuilt netgraph.ko (module-level fix validation)

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