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

OOB read in ng_fixedstring_unparse: known bufSize ignored, delegates to unbounded strlen

Summary

ng_fixedstring_unparse(:785-796): knows fi->bufSize(:789) and advances *off by bufSize(:794), but delegates encoding to ng_string_unparse(:792) which uses unbounded strlen(raw). No NUL in fi->bufSize bytes -> strlen scans past field into adjacent message/heap. Fixedstring types widely used: NG_NODESIZ(32), NG_HOOKSIZ(32), NG_PATHSIZ(512), NG_TYPESIZ(32), NG_CMDSTRSIZ(32) β€” struct ng_mesg header fields + node defs.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0451 Β· 8 files
FileTypeDescriptionSize
ng_fixedstring_oob.c trigger-source NGM_BINARY2ASCII(NGM_NODEINFO/RESP) with 72 non-NUL bytes -> fixedstring OOB 6.0 KB view raw
build.sh build-script cc -O2 -Wall -o ng_fixedstring_oob ng_fixedstring_oob.c -lnetgraph 152 B view raw
run.sh run-script kldload ng_socket.ko; ./ng_fixedstring_oob 804 B view raw
VERDICT.md verdict OOB read repro + fix validation 4.5 KB ↓ raw
fix.diff suggested-fix bound fixedstring strlen by copying bufSize bytes + NUL terminator 1.1 KB view raw
env.txt environment uname, ng_socket.ko loaded 749 B 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
VERDICT.md verdict OOB read repro + fix validation
↓ download raw

DF-0451 β€” ng_fixedstring_unparse() unbounded strlen() OOB kernel heap read

Verdict: REPRODUCED (root-only kernel heap info leak).

Mechanism

sys/netgraph/netgraph/ng_parse.c:785-796:

static int
ng_fixedstring_unparse(const struct ng_parse_type *type,
    const u_char *data, int *off, char *cbuf, int cbuflen)
{
    const struct ng_parse_fixedstring_info *const fi = type->info;
    int error, temp = *off;

    if ((error = ng_string_unparse(type, data, &temp, cbuf, cbuflen)) != 0)
        return (error);
    *off += fi->bufSize;             // <-- advance by bufSize
    return (0);
}

The fixed-string types (NG_NODESIZ=32, NG_HOOKSIZ=32, NG_PATHSIZ=512, NG_TYPESIZ=32, NG_CMDSTRSIZ=32) all use ng_parse_fixedstring_type whose unparse method delegates to ng_string_unparse, which uses unbounded strlen(raw) (ng_parse.c:727). The fixedstring knows fi->bufSize but does NOT bound the strlen with it -- only the offset advance is bounded. With no NUL inside the fi->bufSize bytes, strlen reads past the field into adjacent struct fields and into kernel heap.

PoC trigger path

  1. Open a PF_NETGRAPH control socket -- requires root.
  2. Send NGM_BINARY2ASCII to "." with an inner ng_mesg: typecookie = NGM_GENERIC_COOKIE cmd = NGM_NODEINFO (respType = ng_generic_nodeinfo_type, a struct with two fixedstring fields: name[NG_NODESIZ=32], type[NG_TYPESIZ=32]) flags = NGF_RESP (select respType) arglen = sizeof(struct nodeinfo) = 72 data = 72 'E' bytes (no NUL anywhere)
  3. ng_unparse iterates the struct; for "name" it calls ng_fixedstring_unparse(nodebuf, data, off=0).
  4. strlen(data+0) reads all 72 E's + leaked bytes past the user buffer.
  5. The leak (varies by heap state) appears in the encoded name= field.

Trigger requires root: PF_NETGRAPH sockets need root. Root -> kernel info leak. Not a privilege escalation.

Reproduction (unpatched baseline #0)

First run after kldload (most clear leak):

# ./ng_fixedstring_oob
{ name="EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE5yJyL.s"
  type="EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE5yJyL.s" id=0x45454545 hooks=1162167621 }
[!] fixedstring name= field strlen read 72 'E' bytes (bufSize=32).
OOB READ CONFIRMED: strlen scanned 40 bytes past the bufSize=32 boundary (DF-0451).

The 5yJyL.s (7 bytes) after the 72 E's are leaked kernel heap -- they are not part of the 72-byte user-controlled allocation. name= contains 32 E's from name + 32 E's from type + 4 E's from id + 4 E's from hooks + 7 leaked bytes from heap.

Subsequent runs may leak fewer or 0 bytes depending on heap state (if a NUL byte happens to be adjacent); the bug is still present, just gated by coincidental NULs in heap. Second confirmation run leaked P>\\x99\\x89 (4 bytes).

Fix validation

Applied fix.diff (copy at most fi->bufSize bytes into a local stack buffer, NUL-terminate at [fi->bufSize], then call ng_string_unparse on the local copy so strlen is bounded to exactly the field size). Rebuilt netgraph.ko + kernel, rebooted into 6.5-DEVELOPMENT #1 (2026-07-18):

# ./ng_fixedstring_oob
{ name="EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE" type="EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"
  id=0x45454545 hooks=1162167621 }
[!] fixedstring name= field strlen read 32 'E' bytes (bufSize=32).  strlen stopped
    exactly at bufSize (NUL terminator in input) -- no leak this run.

name= now contains EXACTLY 32 E's (matches bufSize), down from 72+leaked. The fixedstring strlen stopped at the bufSize boundary because the local copy is NUL-terminated at [bufSize] by the fix. Fix is VALIDATED.

Notes

  • DF-0450 (sibling finding) and DF-0451 share root cause: ng_string_unparse's unbounded strlen. DF-0450 covers the bare string type via NGM_TEXT_STATUS; DF-0451 covers the fixedstring wrapper via NGM_NODEINFO's name/type fields.
  • The two fixes are complementary defense-in-depth:
  • DF-0450 (ng_base.c): NUL-terminate the whole binary data area so strlen can't escape the user allocation.
  • DF-0451 (ng_parse.c): bound the fixedstring strlen to bufSize exactly, so even WITHIN the user allocation, a fixedstring can't read past its declared boundary into adjacent struct fields.

Fix verification

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 18 12:48:52 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live). ng_fixedstring_unparse strlen past bufSize=32 -> 7B inter-field OOB read. Root-only PF_NETGRAPH.