# CVE-2026-121941: munt (libmt32emu): heap out-of-bounds write in the MIDI stream parser on an oversized SysEx message
, Post and Telecommunication Institute of Technology
**Project:** [munt/munt]( MT-32 emulator library (libmt32emu), MIDI stream parser
**Affected:** release `libmt32emu_2_8_3` and the `dev` and `master` branches (identical code). File `mt32emu/src/MidiStreamParser.cpp` last modified at commit `b341da457fe00d3daf236a2cb8d639f8a6b7d234`
**Class:** CWE-787 Out-of-bounds Write, CWE-122 Heap-based Buffer Overflow, CWE-131 Incorrect Calculation of Buffer Size
`libmt32emu` parses raw MIDI byte streams through the public class `DefaultMidiStreamParser` and its base method `parseStream`. When a stream begins with a SysEx start byte `0xF0` and the parser buffer is empty, the library calls `parseSysex` to find the end of the message. If the SysEx has no end byte `0xF7` and no embedded status byte, `parseSysex` sets `streamBufferSize` to the full accumulated length and copies that many bytes into the internal heap buffer with `memcpy`. The helper `checkStreamBufferCapacity` grows that buffer only up to a fixed maximum of 32768 bytes and then reports success, without checking that the requested length actually fits. The default buffer capacity is 1000 bytes, which is below the maximum, so the grow path runs. When the SysEx body is longer than 32768 bytes, the `memcpy` writes past the end of the 32768-byte allocation. The number of overflow bytes and their values are controlled by the input, because SysEx data bytes are 7-bit values the caller supplies. This is an out-of-bounds heap write reachable through the documented streaming API under the default configuration, with memory corruption and denial of service as direct consequences.
The parser keeps a single heap buffer `streamBuffer` of size `streamBufferCapacity`, and an amount of valid data `streamBufferSize`. The capacity is bounded between a default and a hard maximum.
`mt32emu/src/globals.h:122` and `:127` define the two sizes, exposed as constants at `:145` and `:148`.
#define MT32EMU_MAX_STREAM_BUFFER_SIZE 32768
const unsigned int MAX_STREAM_BUFFER_SIZE = MT32EMU_MAX_STREAM_BUFFER_SIZE;
const unsigned int SYSEX_BUFFER_SIZE = MT32EMU_SYSEX_BUFFER_SIZE;
The constructor at `mt32emu/src/MidiStreamParser.cpp:71-82` clamps the initial capacity into the range `[SYSEX_BUFFER_SIZE, MAX_STREAM_BUFFER_SIZE]`. `DefaultMidiStreamParser` and `MidiStreamParser` default `initialStreamBufferCapacity` to `SYSEX_BUFFER_SIZE` (1000), see `mt32emu/src/MidiStreamParser.h:64,101,106`.
MidiStreamParserImpl::MidiStreamParserImpl(MidiReceiver &useReceiver, MidiReporter &useReporter, Bit32u initialStreamBufferCapacity) :
midiReceiver(useReceiver), midiReporter(useReporter)
if (initialStreamBufferCapacity >PS: feed one block, start byte F0, over 32768 data bytes, no end byte
PS->>PX: buffer empty and first byte is F0, pass full length
PX->>PX: scan bytes, no end marker found, sysexLength reaches length
PX->>CAP: request capacity for sysexLength
CAP->>HEAP: grow allocation to the fixed maximum of 32768
CAP-->>PX: return true without checking the request fits
PX->>HEAP: memcpy sysexLength bytes into a 32768 byte buffer
HEAP-->>App: write past the end of the allocation, heap corruption
The precondition is a host that uses the documented streaming API and feeds input in blocks, which is the normal way to drive the synth from a MIDI file, a pipe, a socket, or a bulk SysEx dump. The default parser capacity of 1000 bytes is used.
The trigger is one `parseStream` call whose block holds a SysEx that starts with `0xF0`, carries more than 32768 data bytes in `0x00..0x7F`, and never reaches `0xF7` or any other status byte before the block ends. `parseSysex` counts the whole block as one incomplete SysEx, sets `streamBufferSize` to that count, grows the buffer only to 32768, and then copies the full count, writing past the allocation.
Illustrative trigger, not executed during this source review:
// Genuine MT-32 control and PCM ROM images, supplied by the host.
ROMImage *control = ROMImage::makeROMImage(openControlRomFile());
ROMImage *pcm = ROMImage::makeROMImage(openPcmRomFile());
DefaultMidiStreamParser parser(synth); // default capacity 1000, hard max 32768
// One SysEx that opens correctly, never closes, and carries no status byte.
for (int i = 0; i Reported by HaiND (Nguyen Dinh Hai), Post and Telecommunication Institute of Technology, [github.com/Haind03](
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
