OOB kernel heap read in ng_string_unparse via unbounded strlen on binary data without NUL terminator
Summary
ng_string_unparse(:722-735): raw=data+*off(:726), ng_encode_string(raw)(:727) + strlen(raw)+1(:732) scan for NUL with no upper bound. BINARY2ASCII path validates arglen(ng_base.c:1514-1516) but unparse ignores it. Binary data with no NUL in field -> strlen reads past message allocation into kernel heap. Leaked bytes encoded and returned in ASCII response. On 32-bit: strlen*4 can overflow size_t -> small kmalloc + heap write overflow in encoding loop. Netgraph7 passes explicit length; v1 does not.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0450 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ng_oob_leak.c | trigger-source | NGM_BINARY2ASCII with non-NUL-terminated string data -> OOB strlen | 6.7 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o ng_oob_leak ng_oob_leak.c -lnetgraph | 133 B | view raw |
| run.sh | run-script | kldload ng_socket.ko; ./ng_oob_leak | 781 B | view raw |
| VERDICT.md | verdict | OOB read repro + fix validation | 4.0 KB | β raw |
| fix.diff | suggested-fix | NUL-terminate binary->data[arglen] before ng_unparse | 902 B | view raw |
| baseline_before.log | run-log | baseline run: 2 bytes leaked (\x02\x02) | 763 B | view raw |
| after_fix.log | run-log | post-fix run: 0 bytes leaked, arglen 75 -> 67 | 666 B | view raw |
| env.txt | environment | uname, ng_socket.ko loaded | 900 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-0450 β ng_string_unparse() unbounded strlen() OOB kernel heap read
Verdict: REPRODUCED (root-only kernel heap info leak).
Mechanism
sys/netgraph/netgraph/ng_parse.c:722-735:
static int
ng_string_unparse(const struct ng_parse_type *type,
const u_char *data, int *off, char *cbuf, int cbuflen)
{
const char *const raw = (const char *)data + *off;
char *const s = ng_encode_string(raw); // <-- strlen(raw)
...
*off += strlen(raw) + 1; // <-- strlen(raw)
...
}
strlen(raw) is unbounded. When the string field has no NUL terminator,
strlen runs off the end of the user-controlled kmalloc'd buffer into
adjacent kernel heap. ng_encode_string (ng_parse.c:1637) does
kmalloc(strlen(raw) * 4 + 3, ...) -- same OOB.
The trigger surface is NGM_BINARY2ASCII (ng_base.c:1505-1575): a generic
netgraph control message that converts a user-supplied binary ng_mesg to
ASCII. The validation at ng_base.c:1514-1516 only checks the OUTER
arglen vs the INNER arglen; it does NOT ensure the inner data area is NUL-
terminated for the strlen-bound unparser.
The outer message buffer is allocated as kmalloc(len+1, M_NETGRAPH) in
ng_socket.c:253-254, so byte[len] (the +1 byte) plus adjacent heap is
read by strlen when the user data has no internal NUL.
PoC trigger path
- Open a PF_NETGRAPH control socket (NgMkSockNode) -- requires root.
- Send NGM_BINARY2ASCII to "." with an inner ng_mesg: typecookie = NGM_GENERIC_COOKIE cmd = NGM_TEXT_STATUS (respType == &ng_parse_string_type) flags = NGF_RESP (select respType) cmdstr = 32 'C' bytes (no NUL) arglen = 64 data = 64 'D' bytes (no NUL)
- ng_base.c:1562 calls ng_unparse(string_type, binary->data, ...) -> ng_string_unparse -> strlen(raw) reads 64 D's + trailing +1 byte + adjacent heap until a NUL.
- The leaked bytes are encoded into ascii->data and returned to the user.
Trigger requires root: PF_NETGRAPH sockets need root (ng_socket.c). Root -> kernel info leak. Not a privilege escalation.
Reproduction (unpatched baseline #0)
# kldload ng_socket.ko # ./ng_oob_leak === NGM_BINARY2ASCII response === cmd=11 cmdstr=textstatus arglen=75 flags=0x1 ascii arglen=75 === ascii->data (leaked bytes encoded) === "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD\x02\x02"
The trailing \x02\x02 (2 bytes) are NOT in the user-supplied data area
(64 D's); they were read from the +1 uninitialized kmalloc byte + adjacent
heap. ascii arglen = 75 = 1 (open quote) + 64 (D's) + 4 (\x02) + 4 (\x02)
+ 1 (close quote) + 1 (NUL); the legit content was only 67 bytes.
Fix validation
Applied fix.diff (NUL-terminate binary->data[binary->header.arglen]
before ng_unparse, which is safe because the validation at 1514-1516
guarantees that byte lies inside our own allocation), rebuilt netgraph.ko
+ kernel, rebooted into 6.5-DEVELOPMENT #1 (2026-07-18):
# ./ng_oob_leak === NGM_BINARY2ASCII response === cmd=11 cmdstr=textstatus arglen=67 flags=0x1 ascii arglen=67 === ascii->data (leaked bytes encoded) === "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"
arglen dropped 75 -> 67 (exactly the 2 leaked bytes gone). No \\x02 in
the output. Fix is VALIDATED.
Notes
- The leak size varies run-to-run depending on heap state; the first run after kldload may leak more (saw up to 7 bytes when other allocations leave non-NUL residue adjacent). Consistent leak of >=1 byte (the +1 kmalloc slack byte) on every run.
- The same call also drives DF-0451 (the inner ng_mesg struct's cmdstr is a fixedstring that gets strlen-past-bufSize'd); the DF-0450 fix (NUL at end of data area) plus the DF-0451 fix (bounded local copy) together close both. See DF-0451/ for the dedicated PoC that exercises the fixedstring path through NGM_NODEINFO.
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (live). ng_string_unparse unbounded strlen -> 2B heap leak past allocation. Root-only PF_NETGRAPH.
No comments yet.