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)
PoC verification
Evidence pack
findings/poc/DF-0410 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 theNGM_BINARY2ASCIIreply); - the loop writes up to
slen*4+3bytes 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.
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
slenbytes starting atraw, walking pastraw's first NUL terminator into whatever follows in the containing slab allocation. Those bytes are encoded intocbufand returned to userspace via theNGM_BINARY2ASCIIreply β kernel heap info leak. - OOB WRITE (heap overflow): the loop writes up to
slen*4+3bytes into a buffer sized onlystrlen(raw)*4+3. Worst case:raw[0]=='\0'βstrlen(raw)=0βalloc = 0*4+3 = 3 bytes;slen=65535β up to65535*4+3 = 262143bytes 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.kocontrol 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).
strlencall-site count inng_parse.o: VULNERABLE=6 β FIXED=5 (exactly the one removedstrlen(raw)call).- VULNERABLE
ng_encode_stringprologue:callq strlenat0x2056, thenlea 0x3(,%rax,4),%rdi(strlen result Γ4 +3) as the kmalloc size. - FIXED
ng_encode_stringprologue: nocallq strlenbefore kmalloc;lea 0x3(,%rsi,4),%ediusesslen(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;
leanow keyed onslennot 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 vulnerableng_encode_string- harness proving OOB read+write;
-DAPPLY_FIXtoggles 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,nmcount of ng_encode_string = 0).fix.diffβ the git-apply-able one-line fix.manifest.jsonβ artifact catalog.
Fix verification
not_testablenot_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.
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.
No comments yet.