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)
PoC verification
Evidence pack
findings/poc/DF-0451 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
- Open a PF_NETGRAPH control socket -- requires root.
- 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)
- ng_unparse iterates the struct; for "name" it calls ng_fixedstring_unparse(nodebuf, data, off=0).
- strlen(data+0) reads all 72 E's + leaked bytes past the user buffer.
- 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
fixedvalidated
see evidence pack
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.
No comments yet.