Skip to content

gh-160310: Fix quadratic one-shot decompress() with many members - #160311

Merged
gpshead merged 4 commits into
python:mainfrom
gpshead:scanfix-gzip-9r8so5s2
Oct 31, 2026
Merged

gpshead merged 4 commits into
python:mainfrom
gpshead:scanfix-gzip-9r8so5s2

Conversation

@gpshead

@gpshead gpshead commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

The one-shot decompress() functions of gzip, bz2, lzma and compression.zstd gave each new decompressor all of the remaining input and copied the rest back through unused_data, so input with many members took quadratic time. They now feed each member in chunks that start at 1 KiB and double, and step back over unused_data by offset; the first chunk is the whole input, so single-member data still takes one decompressor call. bz2, lzma and zstd share this loop through a private helper in compression._common._streams, while gzip keeps its own because it parses the member header and trailer itself. Results are unchanged for bytes-like input, but the three shared ones now slice a memoryview of the argument, so a non-contiguous memoryview raises TypeError where it raised BufferError, and the TypeError for a non-buffer argument has a different message.

Fixes #160310

ANT-2026-9R8SO5S2

gzip.decompress(), bz2.decompress(), lzma.decompress() and
compression.zstd.decompress() passed all of the remaining input to a new
decompressor for every member and copied the rest back via unused_data.
Feed each member in chunks of increasing size instead.

@gpshead gpshead left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A high level comment I have on this PR... could or should we meaningfully factor out the common shape memoryview loop code used across multiple decompressors?

@emmatyping

Copy link
Copy Markdown
Member

A high level comment I have on this PR... could or should we meaningfully factor out the common shape memoryview loop code used across multiple decompressors?

I think yes, seems reasonable for compression._common!

…ommon

bz2.decompress(), lzma.decompress() and compression.zstd.decompress()
had three copies of the chunked multi-stream loop.  Move it into
compression._common._streams._decompress_all(), which takes the
decompressor factory, the trailing-data error, the stream magic and the
truncation error.  gzip keeps its own loop.
@gpshead
gpshead enabled auto-merge (squash) October 31, 2026 21:21
@gpshead
gpshead merged commit e80962a into python:main Oct 31, 2026
53 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Sprint Oct 31, 2026
@miss-islington-app

Copy link
Copy Markdown

Thanks @gpshead for the PR 🌮🎉.. I'm working now to backport this PR to: 3.14, 3.15.
🐍🍒⛏🤖

@miss-islington-app

Copy link
Copy Markdown

Sorry, @gpshead, I could not cleanly backport this to 3.15 due to a conflict.

Please backport manually with cherry_picker, see the devguide for more information.

cherry_picker e80962a4bd68b162a54fc63cfb754237c2f81d8e 3.15

@miss-islington-app

Copy link
Copy Markdown

Sorry, @gpshead, I could not cleanly backport this to 3.14 due to a conflict.

Please backport manually with cherry_picker, see the devguide for more information.

cherry_picker e80962a4bd68b162a54fc63cfb754237c2f81d8e 3.14

@bedevere-app

bedevere-app Bot commented Oct 31, 2026

Copy link
Copy Markdown

GH-161037 is a backport of this pull request to the 3.15 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.15 bugs and security fixes label Oct 31, 2026
@bedevere-app

bedevere-app Bot commented Oct 31, 2026

Copy link
Copy Markdown

GH-161038 is a backport of this pull request to the 3.14 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.14 bugs and security fixes label Oct 31, 2026
gpshead added a commit that referenced this pull request Oct 31, 2026
…bers (GH-160311) (#161038)

gzip.decompress(), bz2.decompress(), lzma.decompress() and
compression.zstd.decompress() handed each new decompressor all of the
remaining input and copied the rest back through unused_data, so input
made of many concatenated members took quadratic time.  Each member is
now fed in chunks that start at 1 KiB and double, and the loop steps
back over unused_data by offset.  The first chunk is the whole input,
so single-member data still takes one decompressor call.

bz2, lzma and zstd share the loop through a private helper in
compression._common._streams; gzip keeps its own because it parses the
member header, trailer and padding itself.

ANT-2026-9R8SO5S2

(cherry picked from commit e80962a)
gpshead added a commit that referenced this pull request Oct 31, 2026
…bers (GH-160311) (#161037)

gzip.decompress(), bz2.decompress(), lzma.decompress() and
compression.zstd.decompress() handed each new decompressor all of the
remaining input and copied the rest back through unused_data, so input
made of many concatenated members took quadratic time.  Each member is
now fed in chunks that start at 1 KiB and double, and the loop steps
back over unused_data by offset.  The first chunk is the whole input,
so single-member data still takes one decompressor call.

bz2, lzma and zstd share the loop through a private helper in
compression._common._streams; gzip keeps its own because it parses the
member header, trailer and padding itself.

ANT-2026-9R8SO5S2

(cherry picked from commit e80962a)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

One-shot decompress() in gzip, bz2, lzma and compression.zstd is quadratic in the number of members

2 participants