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

Heap buffer overflow in ng_encode_string: buffer allocated by strlen(raw) but loop iterates attacker-controlled slen

Summary

ng_sizedstring_unparse(:924): slen=*(u_int16_t*)(data+*off) attacker-controlled. ng_encode_string(raw,slen)(:925). ng_encode_string(:1832): cbuf=kmalloc(strlen(raw)*4+3,...) β€” alloc by strlen. Loop(:1837) for(i=0;i<slen;i++) writes up to slen*4 bytes. slen>strlen(raw) -> heap overflow. slen=65535+raw starts with NUL: alloc=3 bytes, write=262140. Also OOB read past NUL into heap -> info leak returned to user. Trigger via NGM_BINARY2ASCII on sizedstring type. Root-gated (ng_socket SYSCAP_RESTRICTEDROOT).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0410 Β· 11 files
FileTypeDescriptionSize
df0410_harness.c trigger-source verbatim transcription of vulnerable ng_encode_string + harness proving OOB read+write; -DAPPLY_FIX toggles corrected alloc 9.8 KB view raw
build.sh build-script builds df0410_vuln and df0410_fixed 643 B view raw
run.sh run-script runs both harness builds 895 B view raw
harness_vuln.log run-log full output of vulnerable harness (OOB read+write confirmed) 685 B view raw
harness_fixed.log run-log full output of fixed harness (overflow gone) 704 B view raw
run.log run-log combined vulnerable+fixed harness output 1.4 KB view raw
module_build.log build-log real netgraph7 module built baseline+fixed with -Werror; disassembly shows strlen call removed 6.3 KB view raw
env.txt environment uname, cc 8.3, netgraph module inventory, WANT_NETGRAPH7 absent, nm ng_encode_string count=0 1010 B view raw
fix.diff suggested-fix git-apply-able one-line fix: strlen(raw) -> slen in ng_encode_string kmalloc 386 B view raw
README.md readme reproduction instructions and bug summary 2.6 KB ↓ raw
VERDICT.md verdict full analysis: root cause, dead-code reasoning, root-gate, fix validation 9.8 KB ↓ raw
README.md readme reproduction instructions and bug summary
↓ download raw

DF-0410 β€” PoC reproduction

What this is

A userspace harness that proves the heap OOB read+write primitive in ng_encode_string() (sys/netgraph7/netgraph/ng_parse.c) is real, plus a real-kernel module build that validates the fix compiles and works at the disassembly level.

There is no live kernel PoC because the vulnerable function lives in netgraph7, DragonFly's opt-in parallel netgraph stack, which is NOT compiled into the default X86_64_GENERIC kernel (sys/Makefile.modules builds v1 sys/netgraph/ unless WANT_NETGRAPH7 is defined; verified absent on the guest: nm /boot/kernel/kernel | grep -c ng_encode_string = 0). The bug is also root-gated: the only trigger path (an NGM_BINARY2ASCII control message on a sizedstring field) requires the netgraph7 control socket, whose ngc_attach enforces SYSCAP_RESTRICTEDROOT (ng_socket.c:182). See VERDICT.md for the full analysis.

How to reproduce (harness)

./build.sh     # builds df0410_vuln and df0410_fixed (-DAPPLY_FIX)
./run.sh       # runs both; prints OOB read count + heap overflow size

Expected (vulnerable build)

ng_encode_string returned 25 bytes of encoded output:
  "\x001234567890123456789"
OOB READ: 23 bytes were encoded from BEYOND raw's NUL terminator (strlen(raw)=0 but 23 data bytes appear in output).
OOB WRITE: loop wrote 26 bytes into a 3-byte allocation => 23-byte HEAP OVERFLOW.
[no fix] Heap overflow CONFIRMED: alloc=3 < written=26.

Expected (fixed build)

[APPLY_FIX] allocation now slen*4+3=83 >= 26 written => overflow GONE.

The bug

ng_sizedstring_unparse (ng_parse.c:924) reads an attacker-controlled u_int16_t slen and calls ng_encode_string(raw, slen) (:925). ng_encode_string (:1832) allocates strlen(raw)*4+3 but loops slen times (:1837). If slen > strlen(raw):

  • the loop reads past raw's NUL terminator β†’ kernel heap info leak (encoded bytes returned to userspace via the NGM_BINARY2ASCII reply);
  • the loop writes up to slen*4+3 bytes into the undersized buffer β†’ heap overflow (worst case: raw[0]='\0', slen=65535 β‡’ ~262 KB overflow from a 3-byte allocation).

The fix

fix.diff β€” bound the allocation on slen, not strlen(raw):

-   cbuf = kmalloc(strlen(raw) * 4 + 3, M_NETGRAPH_PARSE,
+   cbuf = kmalloc(slen * 4 + 3, M_NETGRAPH_PARSE,
               M_WAITOK | M_NULLOK);

Validated: applies with git apply -p1; the real netgraph7 module builds under -Werror both ways; disassembly of the fixed object drops the strlen call and keys the allocation on slen. See module_build.log.

VERDICT.md verdict full analysis: root cause, dead-code reasoning, root-gate, fix validation
↓ download raw

DF-0410 β€” Heap OOB read+write in ng_encode_string (netgraph7) β€” REAL primitive, DEAD CODE on default kernel, root-gated

Verdict: REPRODUCED (primitive proven at harness + object level) — latent: opt-in netgraph7 + root-only trigger; NO runtime impact on the default GENERIC kernel; NO uid0 escalation (root→kernel only)

The bug is real at the source level and confirmed in real kernel-compiled code, but it lives in netgraph7 (DragonFly's opt-in parallel netgraph stack), which is not compiled into the default X86_64_GENERIC kernel, and its only trigger path is root-gated (SYSCAP_RESTRICTEDROOT on the netgraph control socket). So on the audit guest β€” and on any default DragonFly install β€” the vulnerable function does not exist in the running kernel, and even on an opt-in netgraph7 system an attacker must already be root to reach it.

Root cause (confirmed line-by-line in sys/netgraph7/netgraph/ng_parse.c)

ng_sizedstring_unparse (the unparse method of ng_parse_sizedstring_type, reached via an NGM_BINARY2ASCII control message on a sizedstring-typed field β€” e.g. ng_pppoe's data field, sys/netgraph7/pppoe/ng_pppoe.h:132):

919: ng_sizedstring_unparse(const struct ng_parse_type *type,
921:     const u_char *data, int *off, char *cbuf, int cbuflen)
922: {
923:   const char *const raw = (const char *)data + *off + 2;   /* bytes AFTER the 2-byte len prefix */
924:   const int slen = *((const u_int16_t *)(data + *off));    /* ATTACKER-CONTROLLED 16-bit length */
925:   char *const s = ng_encode_string(raw, slen);             /* passed straight through */

ng_encode_string (line 1825):

1825: ng_encode_string(const char *raw, int slen)
1832:   cbuf = kmalloc(strlen(raw) * 4 + 3, ...);   /* alloc bounded by strlen(raw) β€” stops at first NUL */
1837:   for (i = 0; i < slen; i++, raw++) {          /* loop bounded by slen β€” independent of strlen */

The mismatch: the allocation is strlen(raw)*4+3 but the loop runs slen times. The attacker controls the 16-bit slen independently of the actual byte content of raw (which can contain embedded NULs). If slen > strlen(raw):

  • OOB READ (info leak): the loop reads slen bytes starting at raw, walking past raw's first NUL terminator into whatever follows in the containing slab allocation. Those bytes are encoded into cbuf and returned to userspace via the NGM_BINARY2ASCII reply β†’ kernel heap info leak.
  • OOB WRITE (heap overflow): the loop writes up to slen*4+3 bytes into a buffer sized only strlen(raw)*4+3. Worst case: raw[0]=='\0' β‡’ strlen(raw)=0 β‡’ alloc = 0*4+3 = 3 bytes; slen=65535 β‡’ up to 65535*4+3 = 262143 bytes written β‡’ ~262 KB heap overflow from a 3-byte allocation (smashes every following object in the kmalloc-4 bucket and well into neighbouring pages).

(The default v1 netgraph ng_encode_string at sys/netgraph/netgraph/ng_parse.c:1632 takes only raw β€” no slen β€” and bounds BOTH alloc and loop on strlen(raw). It is not vulnerable. The bug is specific to the netgraph7 variant.)

Why it is not exploitable on this guest (two independent hard blockers)

1. Dead code on the default kernel (netgraph7 is opt-in)

sys/conf/files gates the file on optional netgraph7 (sys/conf/files:1720), and sys/Makefile.modules builds the v1 sys/netgraph/ tree unless WANT_NETGRAPH7 is defined:

.if defined(WANT_NETGRAPH7)
SUBDIR+=netgraph7
.else
SUBDIR+=netgraph
.endif

WANT_NETGRAPH7 is not in X86_64_GENERIC, not in /usr/src/sys/config/, and not in /etc/make.conf (verified on the guest). share/man/man5/make.conf.5:501 documents it as opt-in ("Set to build a newer, experimental netgraph userland. It has to be accompanied by NETGRAPH7* options in the kernel."). Confirmed on the running 6.5-DEVELOPMENT #0 guest:

  • nm /boot/kernel/kernel | grep -c ng_encode_string β‡’ 0 (the function is entirely absent from the running kernel).
  • /boot/kernel/ contains only v1 modules (netgraph.ko, ng_*.ko); no netgraph7 modules are present.
  • The v1 ng_socket.ko control socket uses the v1 (non-vulnerable) ng_parse.c.

This matches the dead-code pattern documented for DF-0601/DF-0602/DF-0759.

2. Root-only trigger (SYSCAP_RESTRICTEDROOT)

Even on an opt-in netgraph7 system, the only way to deliver an NGM_BINARY2ASCII message to the ng_sizedstring_unparse path is through the netgraph7 control socket, whose ngc_attach requires root:

182:   if (caps_priv_check(ai->p_ucred, SYSCAP_RESTRICTEDROOT | __SYSCAP_NULLCRED))
183:   { error = EPERM; }      /* unprivileged users cannot even create the socket */

(sys/netgraph7/socket/ng_socket.c:182; the v1 socket has the same gate at sys/netgraph/socket/ng_socket.c:172.) So the primitive is root→kernel. Per the bright-line rule, root→kernel is game-over by definition: there is no privilege boundary to cross, and a root attacker who has gone to the trouble of building a WANT_NETGRAPH7 kernel does not need ng_encode_string to escalate.

Net: no uid0 escalation path exists β€” the bug is (a) absent from the default kernel and (b) reachable only from root. It is a defense-in-depth / latent hardening defect: fix it so a future netgraph7 promotion does not ship a trivially-triggerable heap-corruption primitive.

How the primitive was proven (harness + real-kernel object disassembly)

Because the code is not in the running kernel, the primitive was proven by transcribing ng_encode_string verbatim into a userspace harness (df0410_harness.c) and driving it with the exact inputs ng_sizedstring_unparse would supply: a 2-byte u_int16_t length prefix followed by raw bytes, with slen set larger than strlen(raw).

Harness output (vulnerable build)

sized-string payload: strlen(raw)=0, attacker slen=20
VULNERABLE alloc (strlen*4+3) = 3 bytes
FIXED      alloc (slen*4+3)   = 83 bytes
loop iterations (slen)        = 20
ng_encode_string returned 25 bytes of encoded output:
  "\x001234567890123456789"
OOB READ: 23 bytes were encoded from BEYOND raw's NUL terminator (strlen(raw)=0 but 23 data bytes appear in output).
OOB WRITE: loop wrote 26 bytes into a 3-byte allocation => 23-byte HEAP OVERFLOW.
[no fix] Heap overflow CONFIRMED: alloc=3 < written=26.

The output "\x001234567890123456789" is the proof: raw[0] is NUL (strlen(raw)=0) yet 23 data bytes (\x00 + the 19 sentinel digits that followed in the allocation) were encoded β€” i.e. read from beyond the NUL terminator (the info leak) and written into a 3-byte buffer (the overflow). With slen=65535 the same input yields a ~262 KB overflow.

Harness output (fixed build, -DAPPLY_FIX)

Same encoded output, but:

[APPLY_FIX] allocation now slen*4+3=83 >= 26 written => overflow GONE.

Real-kernel object validation

To prove the fix is correct in genuine kernel context (not just the harness), the actual sys/netgraph7/netgraph/ module was built from in-guest /usr/src both with and without the fix, with -Werror:

  • BASELINE_BUILD=OK and FIX_BUILD=OK (both compile cleanly).
  • strlen call-site count in ng_parse.o: VULNERABLE=6 β†’ FIXED=5 (exactly the one removed strlen(raw) call).
  • VULNERABLE ng_encode_string prologue: callq strlen at 0x2056, then lea 0x3(,%rax,4),%rdi (strlen result Γ—4 +3) as the kmalloc size.
  • FIXED ng_encode_string prologue: no callq strlen before kmalloc; lea 0x3(,%rsi,4),%edi uses slen (rsi=2nd arg) directly as the size.

This is machine-level confirmation the fix replaces the strlen-bounded allocation with an slen-bounded one.

Fix

One-line change at sys/netgraph7/netgraph/ng_parse.c:1832: bound the allocation on the loop count (slen) instead of strlen(raw). slen is int sourced from a u_int16_t (range 0–65535), so slen*4+3 max = 262143 β€” a valid kmalloc size with no integer overflow. The loop and read are then in-bounds for all slen.

-   cbuf = kmalloc(strlen(raw) * 4 + 3, M_NETGRAPH_PARSE,
+   cbuf = kmalloc(slen * 4 + 3, M_NETGRAPH_PARSE,
               M_WAITOK | M_NULLOK);

See fix.diff (applies with git apply -p1 / patch -p1; validated).

There is no findings/DF-0410-*.md recommended-fix proposal in this run (no markdown seeded); this fix.diff is the authoritative verified fix.

Fix validation

fix_status: not_testable β€” the running default kernel does not contain ng_encode_string (it is in opt-in netgraph7), so a before/after runtime test on the guest is not meaningful (the file is not in the kernel build, so patching the source and rebuilding the GENERIC kernel changes nothing the PoC can reach). What was validated, beyond git apply --check:

  • the diff applies cleanly to in-guest /usr/src (patch -p1, hunk succeeded);
  • the real netgraph7 module (the actual compilation unit containing the bug) builds with the fix under -Werror;
  • disassembly of the fixed object confirms the strlen-based allocation is eliminated (6β†’5 strlen refs; lea now keyed on slen not the strlen return).

This is the strongest validation possible for a dead-code finding short of building a custom WANT_NETGRAPH7 kernel (which would itself be a non-default build and is not warranted for a root-gated latent bug).

Files

  • df0410_harness.c β€” verbatim transcription of vulnerable ng_encode_string
  • harness proving OOB read+write; -DAPPLY_FIX toggles the corrected alloc.
  • build.sh / run.sh β€” exact build/run commands.
  • harness_vuln.log, harness_fixed.log, run.log β€” full harness output.
  • module_build.log β€” real-kernel module build (baseline + fixed) + disassembly comparison.
  • env.txt β€” guest environment (uname, cc, netgraph module inventory, WANT_NETGRAPH7 absence, nm count of ng_encode_string = 0).
  • fix.diff β€” the git-apply-able one-line fix.
  • manifest.json β€” artifact catalog.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable (dead code). Module build+disasm validated: strlen->slen. Harness BEFORE overflow, AFTER gone.

BEFORE: alloc=3 written=26 overflow. AFTER: alloc=83 >= 26. Module disasm: no strlen call.
↓ fix.diff6.5-DEVELOPMENT #0 (netgraph7 opt-in, nm 0)

Confirmed kernel references

Detail

Exploit chain

none -- dead code + root-only. Valid hard blockers. Primitive: 23B heap overflow from 3B alloc + info leak.

Evidence (decisive lines)

Harness BEFORE: alloc=3 written=26, 23B HEAP OVERFLOW + 23B OOB read. AFTER: alloc=83 >= 26, GONE. Module disasm: strlen call eliminated.

PoC changes

Authored: df0410_harness.c (verbatim ng_encode_string with fix toggle), fix.diff (strlen(raw)->slen at :1832), VERDICT.md, manifest.json.

Verified recommended fix

Change strlen(raw)4+3 to slen4+3 at ng_parse.c:1832. Module builds -Werror, disasm confirms strlen eliminated. Full diff in findings/poc/DF-0410/fix.diff.

Verdict

REPRODUCED (harness+object). ng_sizedstring_unparse reads u16 slen, ng_encode_string allocs strlen(raw)*4+3 but loops slen times. slen>strlen -> 23B heap overflow + OOB read. Dead code: netgraph7 opt-in, nm kernel 0 ng_encode_string. Root-only trigger.