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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0450 Β· 10 files
FileTypeDescriptionSize
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
VERDICT.md verdict OOB read repro + fix validation
↓ download 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

  1. Open a PF_NETGRAPH control socket (NgMkSockNode) -- requires root.
  2. 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)
  3. 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.
  4. 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

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_string_unparse unbounded strlen -> 2B heap leak past allocation. Root-only PF_NETGRAPH.