Heap OOB read in NGM_NAT_PROXY_RULE via non-NUL-terminated user string
| Field | Value |
|---|---|
| ID | DF-0610 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:H |
| CWE | CWE-125 Out-of-bounds Read |
| File | sys/netgraph7/ng_nat.c |
| Lines | 643-654 |
| Area | netgraph7 (NAT node control message handler) |
| Confidence | likely |
| Discovered | 2026-07-02 |
| Reported | pending |
BUILD CAVEAT (read first). This finding is latent: in the current DragonFlyBSD source tree
sys/netgraph7/ng_nat.cis conditionally compiled (optional netgraph7_natinsys/conf/files:1718) but the two dependencies it requires to link are absent: -sys/netinet/libalias/β the directory does not exist on disk (the module callsLibAliasInit/LibAliasProxyRule/LibAliasIn/β¦). -m_megapullup()β referenced atng_nat.c:692, defined nowhere in the tree.So
netgraph7_natas committed cannot be compiled or loaded, and this defect is not reachable on any currently-buildable kernel. It is recorded because (a) it is a real code-level bug that becomes live the moment libalias /m_megapullupare reintroduced or the module is fixed, and (b) it is a one-line fix worth landing alongside any such reintroduction.
Summary
The NGM_NAT_PROXY_RULE handler in ng_nat_rcvmsg casts msg->data
directly to char * and hands it to LibAliasProxyRule() as a C string
with only an arglen >= 6 lower-bound check. Unlike the sibling
NGM_NAT_REDIRECT_* handlers β which bcopy exactly NG_NAT_DESC_LENGTH
bytes and force a NUL terminator β this path performs no length-bounded
copy and no forced NUL termination. The wire data buffer is not
guaranteed to contain a NUL within arglen bytes, so LibAliasProxyRule
walks the heap past the end of the allocation looking for a terminating
NUL.
Root cause
sys/netgraph7/ng_nat.c:643-654:
643: case NGM_NAT_PROXY_RULE:
644: {
645: char *cmd = (char *)msg->data;
646:
647: if (msg->header.arglen < 6) {
648: error = EINVAL;
649: break;
650: }
651:
652: if (LibAliasProxyRule(priv->lib, cmd) != 0)
653: error = ENOMEM;
654: }
655: break;
cmd points at msg->data, which holds exactly msg->header.arglen
attacker-supplied bytes. By contrast, NGM_NAT_REDIRECT_PORT/ADDR/PROTO
all do (lines 423-427, 475-479, 529-533):
bcopy(rp->description, entry->rdr.description, NG_NAT_DESC_LENGTH);
entry->rdr.description[NG_NAT_DESC_LENGTH - 1] = '\0';
explicitly bounding and terminating. The proxy-rule path omits both steps.
LibAliasProxyRule() treats its argument as a C string, so with arglen
non-NUL bytes of attacker data it reads cmd[arglen] and beyond.
The netgraph socket ingress (sys/netgraph7/socket/ng_socket.c:261) does
allocate one slack byte (kmalloc(len + 1, β¦) without M_ZERO), but that
byte is uninitialized heap content; the byte after it (cmd[arglen+1]) is
fully out of the allocation. NG_MKRESPONSE/NG_MKMESSAGE both pass
M_ZERO, so only the socket ingress is affected.
Threat model & preconditions
- Attacker position: local, holding an
AF_NETGRAPHcontrol socket.sys/netgraph7/socket/ng_socket.c:182-184gatesngc_attachoncaps_priv_check(cred, SYSCAP_RESTRICTEDROOT)β effectively root β so this is a root-triggerable bug, not unprivileged. - Privileges gained or impact: kernel heap OOB read. If the walk crosses into an unmapped page the kernel page-faults and panics (local DoS from a root-capable principal). Direct info disclosure to userspace is unlikely because there is no API to read stored proxy rules back out.
- Reachability: send a
NGM_NAT_PROXY_RULEcontrol message whosedataarea is filled with non-NUL bytes and whosearglenmatches the actual sent length (β₯6). With probability ~255/256 the trailing slack byte is non-NUL and the walk crosses the allocation boundary. - Real-world exposure today: NONE β module does not build (see BUILD CAVEAT above).
Proof of concept
PoC source: findings/poc/DF-0610/poc.c.
Build & run (requires a buildable netgraph7_nat + libalias)
cc -o poc poc.c # as root, on a host with a configured ng_nat node named 'nat:' ./poc
Expected output
Kernel panic with a faulting RIP inside LibAliasProxyRule (most likely
outcome), OR the syscall returns success and the rule is stored (latent
corruption if the leaked bytes happen to parse as a valid rule).
Impact
- Blast radius: any host with a loadable
netgraph7_natmodule and a configured NAT node. Today: none, because the module cannot build. - Severity rationale: Low. Root-only trigger, no demonstrated info-leak primitive, deterministic DoS only if the walk hits an unmapped page.
- Reliability of the OOB walk: ~100% (the trailing slack byte is uninitialized and almost always non-NUL).
Recommended fix
Always hand libalias a NUL-terminated string allocated by the handler; do not rely on the framework zero-padding.
--- a/sys/netgraph7/ng_nat.c
+++ b/sys/netgraph7/ng_nat.c
@@ -643,11 +643,22 @@ ng_nat_rcvmsg(node_p node, item_p item, hook_p lasthook)
case NGM_NAT_PROXY_RULE:
{
- char *cmd = (char *)msg->data;
+ char *cmd;
+
+ if (msg->header.arglen < 6) {
+ error = EINVAL;
+ break;
+ }
- if (msg->header.arglen < 6) {
- error = EINVAL;
- break;
+ /*
+ * LibAliasProxyRule() takes a C string. The wire
+ * buffer is not guaranteed to contain a NUL within
+ * arglen bytes, so copy + terminate our own.
+ */
+ cmd = kmalloc(msg->header.arglen + 1, M_NETGRAPH,
+ M_WAITOK | M_NULLOK | M_ZERO);
+ if (cmd == NULL) {
+ error = ENOMEM;
+ break;
}
+ bcopy(msg->data, cmd, msg->header.arglen);
+ cmd[msg->header.arglen] = '\0';
if (LibAliasProxyRule(priv->lib, cmd) != 0)
error = ENOMEM;
+
+ kfree(cmd, M_NETGRAPH);
}
break;
Companion defense-in-depth (belt-and-braces, not required to fix this bug):
pass M_ZERO on the +1 slack byte at
sys/netgraph7/socket/ng_socket.c:261.
References
sys/netgraph7/ng_nat.c:423-427,475-479,529-533β the correct bounded-copy + force-NUL pattern used byNGM_NAT_REDIRECT_*.sys/netgraph7/socket/ng_socket.c:261β thekmalloc(len+1)withoutM_ZEROthat leaves the slack byte uninitialized.sys/netgraph7/socket/ng_socket.c:182-184βSYSCAP_RESTRICTEDROOTgate onngc_attach.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0610 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | AF_NETGRAPH control-socket PoC sending NGM_NAT_PROXY_RULE with 64 non-NUL bytes; compiles & runs but cannot reach the buggy handler (ng_nat unbuildable) | 5.1 KB | view raw |
| build.sh | repro-script | cc -o poc poc.c | 394 B | view raw |
| run.sh | repro-script | ./poc β demonstrates unreachability (sendto ENOENT, no ng_nat node) | 511 B | view raw |
| build.log | build-log | clean compile output (final build, no warnings) | 547 B | view raw |
| run.log | run-log | decisive run: socket() ok after ng_socket kldload, sendto ENOENT (no nat node β ng_nat unbuildable) | 329 B | view raw |
| env.txt | environment | uname, cc version, kern.version, ng_nat module absence | 541 B | view raw |
| fix.diff | suggested-fix | git-apply-able: kmalloc(arglen+1,M_ZERO)+bcopy+force-NUL before LibAliasProxyRule, kfree after; matches sibling REDIRECT_* pattern; applies cleanly (hunk #1 at 642) | 863 B | view raw |
| VERDICT.md | verdict | full narrative: bug real at source, module unbuildable (libalias/ + m_megapullup absent), runtime unreachability confirmed, fix validated as applies+consistent (not_testable runtime) | 6.6 KB | β raw |
| README.md | readme | original PoC README (latent status, preconditions) | 1.5 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0610 β PoC: NGM_NAT_PROXY_RULE heap OOB read via non-NUL-terminated string
Root-capable local DoS / heap OOB read PoC.
Status
LATENT β module does not build in the current tree.
sys/netinet/libalias/ is absent and m_megapullup() (referenced at
ng_nat.c:692) is defined nowhere, so netgraph7_nat cannot be compiled
or loaded. This PoC is source-only until libalias / m_megapullup are
reintroduced.
Files
poc.cβ sends aNGM_NAT_PROXY_RULEcontrol message whosedatais filled with non-NUL bytes and whosearglenmatches the sent length (β₯6), forcingLibAliasProxyRuleto walk the heap past the allocation looking for a NUL terminator.
Build & run (requires a buildable netgraph7_nat + libalias)
cc -o poc poc.c # as root, on a host with a configured ng_nat node named 'nat:' ./poc
Expected outcome
Kernel panic with a faulting RIP inside LibAliasProxyRule (most likely
outcome, when the walk hits an unmapped page); OR the syscall returns
success and the rule is stored (latent corruption if the leaked bytes
happen to parse as a valid rule).
Notes for the per-PoC verifier
- Trigger requires root (
SYSCAP_RESTRICTEDROOTonngc_attachatsys/netgraph7/socket/ng_socket.c:182-184). - Module must be buildable first β libalias +
m_megapullupmust exist. - The fix adds a
kmalloc(arglen+1, β¦, M_ZERO)+bcopy+ forced NUL before handing the string toLibAliasProxyRule; verify withgit apply findings/poc/DF-0610/fix.diff.
DF-0610 β Verdict
Verdict: NOT REPRODUCED (latent β module unbuildable on this kernel)
Impact: none (the buggy code path is unreachable at runtime on the current DragonFlyBSD master tree)
Confidence: certain β confirmed by line-by-line source trace AND by
runtime attempt (kldload ng_nat β "No such file or directory"; the
netgraph7_nat module has no compiled artifact and cannot be built from
the in-tree source).
The bug is REAL at the source level
sys/netgraph7/ng_nat.c:643-654 β the NGM_NAT_PROXY_RULE handler:
643: case NGM_NAT_PROXY_RULE:
644: {
645: char *cmd = (char *)msg->data;
646:
647: if (msg->header.arglen < 6) {
648: error = EINVAL;
649: break;
650: }
651:
652: if (LibAliasProxyRule(priv->lib, cmd) != 0)
653: error = ENOMEM;
654: }
655: break;
cmd points at msg->data, which holds exactly msg->header.arglen
attacker-supplied bytes with no guaranteed NUL terminator.
LibAliasProxyRule() treats its argument as a C string and walks it
looking for a terminating \0, so with non-NUL wire data it reads
cmd[arglen] and beyond β a heap OOB read past the allocation.
The sibling handlers NGM_NAT_REDIRECT_PORT / REDIRECT_ADDR /
REDIRECT_PROTO do it correctly (e.g. ng_nat.c:423-427):
bcopy(rp->description, entry->rdr.description, NG_NAT_DESC_LENGTH);
/* Safety precaution. */
entry->rdr.description[NG_NAT_DESC_LENGTH - 1] = '\0';
β bounded copy + forced NUL. The proxy-rule path omits both steps. The finding's root-cause analysis is accurate.
Why it does NOT reproduce on this kernel
The netgraph7_nat module cannot be compiled or loaded on the current
DragonFlyBSD master tree. Two of its required source dependencies are
absent from sys/ itself:
-
sys/netinet/libalias/β directory does not exist.sys/conf/files:1735-1739declares the module's libalias sources:1735: netinet/libalias/alias.c optional netgraph7_nat 1736: netinet/libalias/alias_db.c optional netgraph7_nat 1737: netinet/libalias/alias_mod.c optional netgraph7_nat 1738: netinet/libalias/alias_proxy.c optional netgraph7_nat 1739: netalias/libalias/alias_util.c optional netgraph7_natNone of these files exist on disk (verified on both hostsys/and guest/usr/src/sys/). The module callsLibAliasInit,LibAliasProxyRule,LibAliasIn, etc. β all unresolved. -
m_megapullup()β referenced atng_nat.c:692, defined nowhere insys/.grep -rn m_megapullup sys/returns only the single reference atng_nat.c:692; there is no definition, so the link fails.
Runtime confirmation (guest 6.5-DEVELOPMENT #0):
# kldload -n ng_nat kldload: can't load ng_nat: No such file or directory rc=1 # find /boot /modules /usr/obj -name 'ng_nat*' (no output β no .ko anywhere)
The PoC (poc.c) compiles cleanly and, after kldload ng_socket,
socket(AF_NETGRAPH, NG_CONTROL) succeeds β but sendto to the "nat"
node fails with ENOENT because no ng_nat node can ever exist on this
kernel:
sendto: No such file or directory (errno=2) If errno=ENOENT/ENODEV the 'nat' node does not exist (ng_nat module not loaded / not buildable on this tree). RUN_EXIT=2
This is case (d) from the procedure: genuinely not reachable on this kernel β the sink is in a module whose source dependencies are missing from the master tree itself (not a config option an operator could flip).
This matches the DF-0611 precedent (same netgraph7_nat module, same
latent classification, same not_reproduced / none verdict).
No escalation chain
The primitive is a heap OOB read, and it is latent (unreachable).
There is no write primitive, no corruption, no chain to develop. Even if
the module were buildable, the trigger requires root
(SYSCAP_RESTRICTEDROOT on ngc_attach at
sys/netgraph7/socket/ng_socket.c:182-184), so there is no privilege
boundary to cross β it would be a rootβkernel local DoS at best.
Fix
fix.diff applies cleanly (patch -p1 --dry-run β rc=0, hunk #1
succeeded at line 642) and closes the OOB read by giving
LibAliasProxyRule() a handler-allocated, NUL-terminated copy of the
wire data β matching the bounded-copy + force-NUL pattern already used
by the sibling NGM_NAT_REDIRECT_* handlers:
cmd = kmalloc(msg->header.arglen + 1, M_NETGRAPH,
M_WAITOK | M_NULLOK | M_ZERO);
if (cmd == NULL) { error = ENOMEM; break; }
bcopy(msg->data, cmd, msg->header.arglen);
cmd[msg->header.arglen] = '\0';
if (LibAliasProxyRule(priv->lib, cmd) != 0)
error = ENOMEM;
kfree(cmd, M_NETGRAPH);
The fix uses only APIs already present and compiling in this exact file
(kmalloc(...,M_NETGRAPH,M_WAITOK|M_NULLOK|M_ZERO) at lines 275/397/456/508;
bcopy at 423/475/529; kfree(...,M_NETGRAPH) at 283/411/467/520).
Fix validation: not_testable
The bug did not reproduce (module unbuildable), so there is no
runtime "before" marker to compare against a patched kernel. The
netgraph7_nat module cannot be built even with the fix applied
(the missing libalias sources and m_megapullup still block the
link), so a single-fix kernel build + reboot test is impossible.
What WAS validated:
- git apply --check passes on the host sys/ tree.
- patch -p1 --dry-run and patch -p1 --forward both succeed on the
guest /usr/src (hunk #1 at line 642).
- The patched source reads correctly (bounded copy + force-NUL before
the LibAliasProxyRule call, kfree after).
- The fix uses only in-file APIs (no new dependencies).
fix_status = not_testable β diff applies + is syntactically consistent
with the file's existing patterns; runtime test impossible because the
module is unbuildable on this tree (latent).
PoC changes
- Wrote
poc.c(the finding's README referenced it but it was never seeded). Self-contained C that opens anAF_NETGRAPHcontrol socket, builds astruct ng_mesgwithtypecookie=NGM_NAT_COOKIE,cmd=NGM_NAT_PROXY_RULE,arglen=64of non-NUL'A'bytes, andsendtos it to a node named "nat". Compiles and runs on the guest; demonstrates unreachability (sendtoβ ENOENT, no ng_nat node). - Wrote
build.sh/run.shrepro scripts. - Authored standalone
fix.diff(git-apply-able, supersedes the finding markdown's proposal with the same logic β clean hunk against current line numbers).
Recommendation
Apply fix.diff to sys/netgraph7/ng_nat.c so the missing-validation
pattern is closed whenever the module is reintroduced. Separately decide
whether to resurrect sys/netinet/libalias/ + m_megapullup() or to
retire netgraph7_nat from sys/conf/files. Severity Low stands as a
latent code-level defect; real-world exposure today is NONE.
Fix verification
not_testablenot_testable: module unbuildable (libalias absent, m_megapullup undefined). fix.diff applies+reads correctly but cannot be runtime-tested.
fix.diff: git apply --check RC=0; patch -p1 Hunk #1 succeeded at 642. Module build impossible (pre-existing missing deps).
Confirmed kernel references
Detail
Exploit chain
none -- latent OOB read, module unbuildable. Even if buildable, root-only trigger.
Evidence (decisive lines)
kldload ng_nat: 'No such file or directory'. ls sys/netinet/libalias/: not found. grep m_megapullup sys/: only ng_nat.c:692 reference, no definition. PoC sendto 'nat': ENOENT.
PoC changes
Wrote poc.c from scratch. Authored fix.diff (bounded kmalloc+bcopy+force-NUL matching sibling handlers). Added build.sh, run.sh, VERDICT.md, manifest.json.
Verified recommended fix
In NGM_NAT_PROXY_RULE handler, replace direct msg->data pass with kmalloc(arglen+1)+bcopy+force-NUL, matching sibling NGM_NAT_REDIRECT_* pattern. Full git-apply-able diff in findings/poc/DF-0610/fix.diff.
Verdict
NOT REPRODUCED -- latent. Bug is REAL at source level (ng_nat.c:643-654 passes msg->data directly to LibAliasProxyRule with no NUL-termination), but netgraph7_nat module CANNOT be compiled: sys/netinet/libalias/ absent from tree, m_megapullup() undefined. kldload ng_nat -> ENOENT. Same class as DF-0611.
No comments yet.