# CVE-2026-122328, CVE-2026-122329: SDL_mixer FetchXLaw Stack Buffer Overflow in WAV and AIFF Decoders
, Post and Telecommunication Institute of Technology
**Project:** [libsdl-org/SDL_mixer]( SDL3_mixer audio mixing library
**Affected:** SDL3_mixer release-3.2.4 (and all SDL3_mixer releases from 3.2.0), pinned at commit `72a81869b45e249e8e67102db4e98dd2441f05a1`
The `FetchXLaw` function in SDL3_mixer, used by both the WAV and AIFF decoders to expand ulaw/alaw encoded audio into float samples, contains a pointer arithmetic error that causes a stack buffer overflow of up to approximately 1020 bytes. The function reads up to 256 bytes of compressed data into a 1024-byte stack buffer, then attempts to expand each byte to a 4-byte float in place by iterating backward. The output pointer is initialized to `&buffer[(length - 1) * 4]` and then indexed by `[i]`, which double-counts the offset: write `out[i]` goes to byte `(length - 1) * 4 + i * 4` instead of byte `i * 4`. For `length = 256` and `i = 255`, the write lands at byte 2040, which is 1016 bytes past the end of the buffer. The overflow is reachable in default configuration from any WAV file declaring MULAW (format tag 0x0007) or ALAW (0x0006) encoding, or any AIFC file declaring ulaw or alaw compression. No special options or flags are required. The data written to the stack comes from a float lookup table, giving the attacker deterministic control over the overflow content by choosing encoded audio bytes.
Both the WAV and AIFF decoders use the same pattern for ulaw and alaw audio: read N bytes of compressed data (one byte per sample), then expand each byte to a 4-byte float using a lookup table, writing the result back into the same buffer. The buffer is sized to hold the expanded output (4N bytes for N input samples). The expansion must run backward, from the last sample to the first, to avoid overwriting not-yet-converted input bytes.
In `src/decoder_aiff.c`, lines 86-98 (identical code in `src/decoder_wav.c`, lines 750-762):
static int FetchXLaw(AIFF_TrackData *tdata, Uint8 *buffer, int buflen, const float *lut)
length = (int) SDL_ReadIO(tdata->io, buffer, (size_t)(length / 4));
if (length % tdata->adata->framesize != 0) {
length -= length % tdata->adata->framesize;
float *out = (float *) &buffer[(length - 1) * 4]; // BUG: wrong base
out[i] = lut[buffer[i]]; // writes to (length-1)*4 + i*4
`SDL_ReadIO` reads at most `buflen / 4` bytes into `buffer`. With `buflen = 1024` (the caller's stack buffer size), `length` is at most 256.
The output pointer `out` is set to `(float *) &buffer[(length - 1) * 4]`. For `length = 256`, this points to byte 1020 in the buffer. Then `out[i]` accesses byte `1020 + i * 4`. When `i = 255`, the write goes to byte `1020 + 1020 = 2040`, which is 1016 bytes past the 1024-byte buffer.
### The correct pattern (sibling contrast)
`FetchFloat64BE` in the same file (`src/decoder_aiff.c:168-177`) performs a similar in-place conversion (double to float) correctly:
static int FetchFloat64BE(AIFF_TrackData *tdata, Uint8 *buffer, int buflen)
length = (int) SDL_ReadIO(tdata->io, buffer, (size_t)length);
float *out = (float *) buffer; // CORRECT: base at buffer[0]
for (i = 0, o = 0; i = 0; i -= 3, o -= 4) {
float *out = (float *) &buffer[o]; // CORRECT: o is computed per-step
Both callers allocate a 1024-byte stack buffer.
`AIFF_decode` (`src/decoder_aiff.c:531-549`):
static bool SDLCALL AIFF_decode(void *track_userdata, SDL_AudioStream *stream)
AIFF_TrackData *tdata = (AIFF_TrackData *) track_userdata;
const int br = tdata->adata->fetch(tdata, buffer, buflen);
`WAV_decode` (`src/decoder_wav.c:1600-1672`):
static bool SDLCALL WAV_decode(void *track_userdata, SDL_AudioStream *stream)
const int br = tdata->adata->fetch(tdata, buffer, buflen);
**WAV path** (`src/decoder_wav.c:885-890`), in `ParseFMT`:
**AIFF path** (`src/decoder_aiff.c:397-404`), in `AIFF_init_audio_internal`:
No special option or configuration is needed. The format tag in the file header selects the code path.
Attacker->>App: Deliver crafted WAV file with MULAW encoding
Mixer->>WAV: WAV_init_audio (ParseFMT sets fetch = FetchULaw)
Mixer->>WAV: WAV_decode allocates 1024-byte stack buffer
WAV->>XLaw: FetchULaw calls FetchXLaw(buffer, 1024)
XLaw->>XLaw: Reads 256 bytes of ulaw data into buffer
XLaw->>XLaw: Sets out pointer to buffer plus 1020 bytes
XLaw->>XLaw: Writes out[255] at byte 2040, 1016 past buffer end
XLaw-->>WAV: Stack corruption, saved return address overwritten
The trigger requires a minimal WAV file with the MULAW format tag and at least 256 bytes of audio data. The file structure is:
1. RIFF header: 12 bytes (magic "RIFF", file size, "WAVE")
2. fmt chunk: 26 bytes (format tag 0x0007 for MULAW, 1 channel, 8000 Hz sample rate, 8 bits per sample)
3. data chunk: header (8 bytes) plus 256 or more bytes of arbitrary audio data
When the application calls `Mix_LoadWAV` on this file, `ParseFMT` reads the format tag 0x0007 and sets `adata->fetch = FetchULaw`. On playback, `WAV_decode` allocates a 1024-byte stack buffer and calls `FetchULaw`, which calls `FetchXLaw`. The function reads up to 256 bytes from the data chunk into the buffer, then iterates from `i = 255` down to `i = 0`. Each iteration writes a float to `buffer[(255) * 4 + i * 4]`. The write at `i = 255` goes to byte 2040, overwriting the stack frame of `WAV_decode` including the saved return address.
The attacker controls the float values written: each position in the overflow region is determined by the corresponding ulaw-encoded byte in the audio data, expanded through the fixed `MIX_ulawToFloat` lookup table. The 256 possible float values span [-1.0, 1.0] in the IEEE 754 encoding.
The same attack applies to the AIFF decoder using an AIFC file with ulaw compression.
Source-confirmed at commit `72a81869b45e249e8e67102db4e98dd2441f05a1` (release-3.2.4, also current HEAD of `main`). Runtime verification with AddressSanitizer was not executed in this phase.
The overflow arithmetic is deterministic from the source:
- `length` = min(256, bytes available in data chunk)
- Output base: `&buffer[(length - 1) * 4]` = `&buffer[1020]` when `length = 256`
- Maximum write: `out[255]` at `&buffer[1020 + 1020]` = `&buffer[2040]`
- Overflow distance: 2040 + 4 - 1024 = 1020 bytes past the end
The correct code would set `out = (float *) buffer`, placing `out[255]` at byte 1020, which fits within 1024 bytes. This is exactly what the sibling function `FetchFloat64BE` does at line 172.
Expected AddressSanitizer output on a runtime test:
- WRITE of size 4 at address [stack buffer + 1020 to 2043]
- in FetchXLaw at `decoder_wav.c:757` (or `decoder_aiff.c:93`)
- stack-buffer-overflow on address 0x... which is located at offset ... from 'buffer' declared at `decoder_wav.c:1658`
**Stack buffer overflow:** The `FetchXLaw` function writes up to ~1020 bytes past the end of a 1024-byte stack buffer. The overflow data comes from a 256-entry float lookup table, giving the attacker deterministic control over the byte patterns written to the stack.
**Attack surface:** Any application using SDL3_mixer to load untrusted WAV or AIFF audio files. SDL_mixer is used by thousands of games and multimedia applications. The trigger is a standard WAV file with a valid but uncommon encoding format (MULAW or ALAW), which any media player or game engine will attempt to decode.
**With stack canaries (default on modern compilers):** The overflow corrupts the canary, producing a deterministic crash (stack smashing detected). This is a denial-of-service from a crafted audio file.
**Without stack canaries:** The overflow corrupts the saved return address of `WAV_decode` or `AIFF_decode`. The attacker-controlled float values limit the byte patterns available for the return address, but partial overwrite or architecture-specific techniques may allow control-flow hijacking.
**Not established:** Instruction-pointer control, arbitrary code execution, and information disclosure were not demonstrated. The float lookup table constrains the byte patterns available for overwriting control data, making exploitation architecture-dependent.
**Where exactly:** `src/decoder_aiff.c:93` and `src/decoder_wav.c:757`
- float *out = (float *) &buffer[(length - 1) * 4];
The fix changes the output base pointer from an offset position to the start of the buffer. With `out = (float *) buffer`, the write `out[i]` goes to byte `i * 4`, placing the last float at byte `(length - 1) * 4 = 1020`, which fits within the 1024-byte buffer. The backward iteration still avoids overwriting unconverted input because the input byte at index `i` (byte `i`) is always before the output float at index `i` (byte `i * 4`).
**Short-term:** Apply the one-line fix above in both files.
**Medium-term:** Refactor FetchXLaw, FetchPCM24, and FetchFloat64 into a shared in-place expansion helper to prevent future copy-paste errors between the WAV and AIFF decoders.
**Defense-in-depth:** Add a bounds assertion or clamp to verify that the output pointer never exceeds `buffer + buflen` during the expansion loop.
- SDL_mixer repository:
- Pinned source (AIFF decoder):
- Pinned source (WAV decoder):
- SDL3_mixer release-3.2.4:
- Related closed issues (distinct bugs): #550 (ADPCM buffer, 2023), #299 (Timidity off-by-one, 2021), #206 (MS ADPCM double-free, 2021)
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.
