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

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.c is conditionally compiled (optional netgraph7_nat in sys/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 calls LibAliasInit/LibAliasProxyRule/LibAliasIn/…). - m_megapullup() β€” referenced at ng_nat.c:692, defined nowhere in the tree.

So netgraph7_nat as 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_megapullup are 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_NETGRAPH control socket. sys/netgraph7/socket/ng_socket.c:182-184 gates ngc_attach on caps_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_RULE control message whose data area is filled with non-NUL bytes and whose arglen matches 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_nat module 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).

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

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0610 Β· 11 files
FileTypeDescriptionSize
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
README.md readme original PoC README (latent status, preconditions)
↓ download 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 a NGM_NAT_PROXY_RULE control message whose data is filled with non-NUL bytes and whose arglen matches the sent length (β‰₯6), forcing LibAliasProxyRule to 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_RESTRICTEDROOT on ngc_attach at sys/netgraph7/socket/ng_socket.c:182-184).
  • Module must be buildable first β€” libalias + m_megapullup must exist.
  • The fix adds a kmalloc(arglen+1, …, M_ZERO) + bcopy + forced NUL before handing the string to LibAliasProxyRule; verify with git apply findings/poc/DF-0610/fix.diff.
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)
↓ download raw

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:

  1. sys/netinet/libalias/ β€” directory does not exist. sys/conf/files:1735-1739 declares 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_nat None of these files exist on disk (verified on both host sys/ and guest /usr/src/sys/). The module calls LibAliasInit, LibAliasProxyRule, LibAliasIn, etc. β€” all unresolved.

  2. m_megapullup() β€” referenced at ng_nat.c:692, defined nowhere in sys/. grep -rn m_megapullup sys/ returns only the single reference at ng_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 an AF_NETGRAPH control socket, builds a struct ng_mesg with typecookie=NGM_NAT_COOKIE, cmd=NGM_NAT_PROXY_RULE, arglen=64 of non-NUL 'A' bytes, and sendtos it to a node named "nat". Compiles and runs on the guest; demonstrates unreachability (sendto β†’ ENOENT, no ng_nat node).
  • Wrote build.sh / run.sh repro 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_testable
baseline no→ patch + rebuild →patched clean

not_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).
↓ fix.diffn/a -- module unbuildable, no runtime test possible

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.