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

Heap buffer overflow in ng_string_parse: missing *buflen bounds check before bcopy of user-supplied string

Summary

ng_string_parse(:704-720): bcopy(sval,buf,len)(:716) where len=strlen(sval)+1 with NO check len<=*buflen. Every sibling parse function checks (int8_getDefault:385, fixedstring_parse:774, bytearray_parse:959). NGM_ASCII2BINARY(ng_base.c:1578-1648): bufSize=2000, attacker NGF_RESP+NGM_TEXT_CONFIG/STATUS -> respType=string_type. String >2000 bytes -> bcopy overflows binary->data 2000-byte alloc into adjacent heap. arglen set to inflated size(:1644) -> secondary OOB read on response copy. Distinct from DF-0410 (netgraph7 unparse direction). Root-gated ng_socket SYSCAP_RESTRICTEDROOT.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0449 Β· 14 files
FileTypeDescriptionSize
ng_overflow.c trigger-source NGM_ASCII2BINARY trigger: 2500-char string -> 501-byte heap overflow; clean ERANGE message on fixed kernels 8.1 KB view raw
build.sh build-script cc -o ng_overflow ng_overflow.c 169 B view raw
run.sh run-script loads modules + runs ng_overflow as root 475 B view raw
README.md readme human-facing reproduction guide + impact analysis 4.6 KB ↓ raw
VERDICT.md verdict full root-cause analysis, trigger trace, fix before/after, 2026-07-16 re-validation note 8.1 KB ↓ raw
run.log run-log fresh 2026-07-16 baseline (unpatched #0) run: arglen=2553>2000 overflow confirmed 3.2 KB view raw
fix_run.log run-log patched-module (sha256 6e225a12...) run: sendto ERANGE, clean 'bounds check fired (FIXED)' message 271 B view raw
fix_build.log build-log standalone netgraph.ko build (rc=0) + patched-module sha256 6e225a12... 7.7 KB view raw
leak_sample.txt leak-sample hex of overflowed/OOB-read bytes (attacker's own A's, not pre-existing kernel data) 2.0 KB view raw
env.txt environment uname, cc version, module hashes, INVARIANTS=1 note 405 B view raw
fix.diff suggested-fix add len>*buflen -> ERANGE check (with kfree) before bcopy in ng_string_parse, mirroring all sibling parse functions 385 B view raw
manifest.json manifest this file 3.6 KB view 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 human-facing reproduction guide + impact analysis
↓ download raw

DF-0449 β€” Heap buffer overflow in ng_string_parse (CWE-787)

Severity: High (root→kernel heap corruption; root-gated trigger) Class: CWE-787 Out-of-bounds Write + secondary CWE-125 OOB Read File/lines: sys/netgraph/netgraph/ng_parse.c:704-720 (ng_string_parse) Caller: sys/netgraph/netgraph/ng_base.c:1578-1648 (NGM_ASCII2BINARY)

The bug

ng_string_parse() parses a quoted netgraph string and copies the decoded result into the caller-supplied buffer buf of size *buflen. It computes len = strlen(sval) + 1 and unconditionally executes bcopy(sval, buf, len) at line 716 with no check that len <= *buflen. Every sibling parse function in the same file performs this bounds check before its bcopy:

Function Check Line
ng_int8_getDefault if (*buflen < sizeof(int8_t)) return (ERANGE) 385
ng_int16_getDefault if (*buflen < sizeof(int16_t)) return (ERANGE) 479
ng_int32_getDefault if (*buflen < sizeof(int32_t)) return (ERANGE) 573
ng_int64_getDefault if (*buflen < sizeof(int64_t)) return (ERANGE) 666
ng_string_getDefault if (*buflen < 1) return (ERANGE) 742
ng_fixedstring_parse if (strlen(sval)+1 > fi->bufSize) return (E2BIG) 774
ng_string_parse NONE 716

Trigger path (root-only)

  1. socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL) β€” requires SYSCAP_RESTRICTEDROOT (ng_socket.c:172), i.e. root.
  2. sendto(csock, NGM_ASCII2BINARY_msg, ..., ".") β€” sends to own socket node.
  3. NGM_ASCII2BINARY (ng_base.c:1578) allocates bufSize=2000, builds a response with binary->data = 2000-byte buffer.
  4. With NGF_RESP set in the embedded message and cmdstr="textstatus", argstype = c->respType = &ng_parse_string_type (ng_base.c:1631-1632).
  5. ng_parse() β†’ ng_string_parse() β†’ bcopy(sval, buf, len) where len = strlen(sval)+1. A string of 2500 chars β‡’ len=2501 β‡’ 501-byte heap overflow past the 2000-byte binary->data into the adjacent slab object.

Secondary OOB read: ng_base.c:1644 sets arglen to the inflated len; ship_msg (ng_socket.c:737) does m_devget(msg, sizeof(ng_mesg)+arglen) = reads 2605 bytes from a 2104-byte allocation β‡’ 501-byte OOB read of adjacent heap. Note: the OOB-read window always equals the overflow window (len-2000), so it reads back the attacker's own overflowed bytes β€” not pre-existing kernel data. The primary impact is the heap corruption (CWE-787).

How to reproduce

./build.sh                        # cc -o ng_overflow ng_overflow.c
# as root:
kldload netgraph && kldload ng_socket
./run.sh                          # ./ng_overflow (run as root)

Expected output (bug present β€” unpatched #0 kernel)

[*] sending NGM_ASCII2BINARY: string=2500 A's, bufSize(buflen)=2000, expected overflow=501 bytes
[+] sendto returned 2607 bytes
[+] recvfrom returned 2605 bytes (response)
[*] response header.arglen = 2553 (alloc was 2000)
[!!] OVERFLOW CONFIRMED: arglen=2553 > 2000; secondary OOB read leaked ~553 bytes of adjacent heap

Expected output (FIXED kernel/module)

[*] sending NGM_ASCII2BINARY: string=2500 A's, bufSize(buflen)=2000, expected overflow=501 bytes
[!] sendto failed: Result too large          ← ERANGE from the new bounds check

Impact

Root → kernel heap corruption. The trigger is root-gated (SYSCAP_RESTRICTEDROOT on the ng_socket control domain), so this is not an unprivileged-to-root escalation. It is a root→kernel integrity violation: a root process (including a compromised setuid-root program or a jail context where root is available but kernel isolation should hold) can corrupt arbitrary adjacent slab objects with attacker-controlled bytes. A successful slab-grooming escalation to uid=0-equivalent kernel compromise is conceivable, but is blocked on this audit guest by INVARIANTS (KKASSERT panics on slab free-list corruption, proven by DF-0783/DF-0028). On a production kernel without INVARIANTS, the 501-byte attacker-controlled heap write is a viable corruption primitive.

Files

  • ng_overflow.c β€” trigger PoC (sends oversized NGM_ASCII2BINARY)
  • build.sh / run.sh β€” reproducible build/run
  • run.log β€” baseline (unpatched) run, full output
  • fix_run.log β€” patched-module run, full output
  • leak_sample.txt β€” hex of the overflowed/OOB-read bytes
  • env.txt β€” guest environment
  • fix.diff β€” the fix (add len > *buflen check, mirroring siblings)
  • fix_build.log β€” module build + disassembly verification
  • VERDICT.md β€” full analysis
  • manifest.json β€” artifact catalog
VERDICT.md verdict full root-cause analysis, trigger trace, fix before/after, 2026-07-16 re-validation note
↓ download raw

DF-0449 β€” VERDICT

Verdict: REPRODUCED β€” heap buffer overflow confirmed; fix validated.

Re-validated 2026-07-16. Fresh vm.sh reset with-src baseline (#0 unpatched kernel, netgraph.ko sha256 85938016…), rebuilt the PoC, and re-confirmed the overflow (arglen=2553 > 2000, 501-byte heap write, guest survived). Then re-applied fix.diff to in-guest /usr/src, rebuilt netgraph.ko standalone (make in sys/netgraph/netgraph/, rc=0), got the identical patched-module hash 6e225a12… (deterministic build), verified the new cmp (%rbx),%eax; jg <ERANGE> in the disassembly at offset 0x3b43, installed it, rebooted, and re-ran 3Γ— β€” every run returned ERANGE (Result too large), no overflow, guest healthy. The fix is deterministic and reproducible. (The PoC source was lightly edited to print a clean ERANGE: bounds check fired (FIXED) message on the fixed path so the "fixed" output is unambiguous; the trigger logic is unchanged.)

Root cause

ng_string_parse() (sys/netgraph/netgraph/ng_parse.c:704-720) copies a user-supplied decoded string into a caller-provided buffer without checking that the string length fits the buffer:

static int
ng_string_parse(const struct ng_parse_type *type,
    const char *s, int *off, const u_char *const start,
    u_char *const buf, int *buflen)
{
    char *sval;
    int len;

    if ((sval = ng_get_string_token(s, off, &len)) == NULL)
        return (EINVAL);
    *off += len;
    len = strlen(sval) + 1;
    bcopy(sval, buf, len);          /* ← line 716: NO check len <= *buflen */
    kfree(sval, M_NETGRAPH);
    *buflen = len;
    return (0);
}

Every sibling parse/getDefault function in this file checks *buflen before its bcopy and returns ERANGE/E2BIG on overflow: - ng_int8_getDefault:385, ng_int16_getDefault:479, ng_int32_getDefault:573, ng_int64_getDefault:666, ng_string_getDefault:742 β€” all if (*buflen < sizeof(...)) return (ERANGE) - ng_fixedstring_parse:774 β€” if (strlen(sval)+1 > fi->bufSize) return (E2BIG)

ng_string_parse is the sole exception. This is a clear oversight.

Trigger path (confirmed line-by-line)

  1. socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL) β€” root-only (ng_socket.c:172: caps_priv_check(..., SYSCAP_RESTRICTEDROOT)).
  2. sendto(csock, msg, ..., ".") β†’ ngc_send (ng_socket.c:201) β†’ ng_send_msg (ng_base.c:1208) β†’ ng_path2node resolves "." to own node (ng_base.c:1216).
  3. CALL_MSG_HANDLER (ng_base.c:1188): typecookie == NGM_GENERIC_COOKIE β‡’ ng_generic_msg.
  4. case NGM_ASCII2BINARY (ng_base.c:1578): bufSize = 2000 (hardcoded, line 1580). NG_MKRESPONSE(rp, msg, sizeof(*binary)+bufSize, ...) (line 1598) allocates 52+52+2000 = 2104 bytes. binary->data is the 2000-byte buffer.
  5. bcopy(ascii, binary, sizeof(*ascii)) (line 1606) copies the attacker's embedded message header β€” including flags = NGF_RESP β€” into binary.
  6. Command lookup (line 1609-1624): cmdstr = "textstatus" matches the generic cmd whose respType = &ng_parse_string_type (ng_base.c:292).
  7. argstype = (binary->header.flags & NGF_RESP) ? c->respType : c->mesgType (line 1631-1632) β‡’ argstype = &ng_parse_string_type.
  8. ng_parse(argstype, ascii->data, &off, binary->data, &bufSize) (line 1636) β‡’ ng_string_parse β‡’ bcopy(sval, buf, 2501) into 2000-byte buf (line 716) β‡’ 501-byte heap overflow.
  9. *buflen = 2501 (line 718) β‡’ binary->header.arglen = 2501 (line 1644), rp->header.arglen = 2553 (line 1645).
  10. ship_msg (ng_socket.c:737): msglen = 52+2553 = 2605; m_devget(msg, 2605, ...) reads 2605 bytes from the 2104-byte rp allocation β‡’ 501-byte OOB read of adjacent heap (secondary CWE-125).

Evidence (unpatched #0 kernel)

[*] sending NGM_ASCII2BINARY: string=2500 A's, bufSize(buflen)=2000, expected overflow=501 bytes
[+] sendto returned 2607 bytes
[+] recvfrom returned 2605 bytes (response)
[*] response header.arglen = 2553 (alloc was 2000)
[!!] OVERFLOW CONFIRMED: arglen=2553 > 2000

Deterministic across 3 consecutive runs; guest survived all runs (the bcopy itself is a raw memcpy, not INVARIANTS-guarded).

Note on the "info leak"

The secondary OOB read (step 10) reads back the same bytes the bcopy overflow wrote (both windows = len - 2000 = 501 bytes). So it returns the attacker's own 'A' bytes, not pre-existing kernel data. The leak is technically a CWE-125 but is not a useful information disclosure. The primary security impact is the CWE-787 heap overflow β€” 501 attacker-controlled bytes corrupting the adjacent slab object.

Exploit chain / escalation analysis

The primitive is a root β†’ kernel heap write of 501 attacker-controlled bytes (full content control β€” the decoded string is arbitrary) into the slab object adjacent to a kmalloc(2104) allocation (page-zone, since 2048 < 2104 ≀ 4096).

uid=0 escalation: NOT attempted β€” valid hard blocker (INVARIANTS).

On this audit guest, INVARIANTS is ENABLED (KKASSERT/KASSERT are CPP macros that expand to inline panic(), so nm | grep KASSERT = 0 is a false negative; proven by DF-0783's _kfree assertion panic and DF-0028's KKASSERT panic). A slab-grooming exploitation chain (spray the 4K zone β†’ overflow into a victim object containing a function pointer / ucred * / refcount β†’ convert to arbitrary control) would require double-free or free-list manipulation, both of which trip INVARIANTS assertions and panic the kernel before the chain can complete. This is the same hard wall documented for DF-0783.

The overflow itself is not INVARIANTS-blocked (it is a bcopy, not a slab operation), so the corruption reproduces cleanly and the guest survives β€” but converting the corruption to uid=0 via slab grooming is blocked. This is a valid hard blocker per the procedure (INVARIANTS prevents the grooming/free-list phase of any heap-corruption-to-priv-esc chain on this guest).

On a production kernel without INVARIANTS, this 501-byte attacker-controlled heap write would be a viable corruption primitive: spray the 4K slab zone with victim objects containing function pointers (e.g., struct file fileops, socket ops vectors), position one adjacent to the overflow target, overwrite its function pointer, and redirect to userspace shellcode (no SMEP) or a forged ucred (no SMAP). The root-gating limits this to root→kernel, but that is still a meaningful kernel-integrity / sandbox-escape primitive.

The fix

Add the missing bounds check before the bcopy, mirroring every sibling parse function. The fix returns ERANGE (matching ng_string_getDefault:742, ng_int*_getDefault) and frees sval first to avoid a memory leak:

    len = strlen(sval) + 1;
+   if (len > *buflen) {
+       kfree(sval, M_NETGRAPH);
+       return (ERANGE);
+   }
    bcopy(sval, buf, len);

(ng_fixedstring_parse:775 has the same missing-free on its E2BIG path β€” a pre-existing minor leak we do not need to fix here, but worth noting.)

Fix validation (Phase 8)

kernel / module result
Before unpatched #0, original netgraph.ko overflow: arglen=2553 > 2000, 501-byte heap write + OOB read
After same #0 kernel, patched netgraph.ko (sha256 6e225a…) ERANGE: sendto failed: Result too large, no overflow, guest alive

The fix was validated by rebuilding only netgraph.ko (standalone module build: cd /usr/src/sys/netgraph/netgraph && make), installing it to /boot/kernel/netgraph.ko, and loading it on a fresh boot (no kernel reboot needed β€” the module is not loaded until kldload). The fix is visible in the disassembly of ng_string_parse: new cmp (%rbx),%eax; jg <ERANGE_path> at offset 0x3b43. The patched module returns ERANGE deterministically across 3 runs.

Verdict

REPRODUCED. The heap buffer overflow in ng_string_parse is real, confirmed by the inflated arglen=2553 and the 501-byte overflow. The fix (add len > *buflen β†’ ERANGE) closes it completely, validated before/after on the same #0 kernel with only the netgraph.ko module swapped.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline arglen=2553 overflow; patched ERANGE. Module disasm confirms bounds check. 3x clean.

BEFORE: OVERFLOW CONFIRMED. AFTER: ERANGE no overflow.
↓ fix.diff6.5-DEVELOPMENT #0 (module-only swap netgraph.ko)

Confirmed kernel references

Detail

Exploit chain

none -- root->kernel (SYSCAP_RESTRICTEDROOT). 501B heap write root->kernel integrity violation. INVARIANTS blocks slab grooming on GENERIC.

Evidence (decisive lines)

BEFORE: arglen=2553>2000 OVERFLOW CONFIRMED. AFTER: ERANGE sendto failed. Disasm: cmp (%rbx),%eax; jg ERANGE.

PoC changes

Authored: ng_overflow.c (NGM_ASCII2BINARY with oversized string), fix.diff (if(len>*buflen){kfree(sval);return ERANGE;}), VERDICT.md, manifest.json.

Verified recommended fix

Add if(len>*buflen){kfree(sval,M_NETGRAPH);return(ERANGE);} at ng_parse.c:716 before bcopy. Mirrors all sibling parse functions. Full diff in findings/poc/DF-0449/fix.diff.

Verdict

REPRODUCED. ng_string_parse ng_parse.c:716 bcopy(sval,buf,len) len=strlen(sval)+1 no check vs *buflen=2000. Every sibling checks. NGM_ASCII2BINARY with 2500-char string -> 501B heap overflow (arglen=2553>2000). Root-only (SYSCAP_RESTRICTEDROOT on ng_socket). Old netgraph v1 (shipped).