Skip to content

Compress(byte[]) throws for incompressible input over ~280 KB: GetMaxCompressedLength undercounts Deflate/Gzip/ZLib overhead #39

Closed

Description

@matt-edmondson

What's wrong

The Deflate, Gzip, ZLib and Brotli providers compute GetMaxCompressedLength as n + ((n / 65535) + 1) * 5 + 64 (for example in Essentials.CompressionProviders.Deflate/DeflateCompressionProvider.cs:23-24). The remark says this is because incompressible input goes into 64 KB stored blocks with a 5-byte header each.

zlib at the default and Optimal level doesn't do that. It closes a block every time its literal buffer fills, which is roughly every 16 KB. So the per-block overhead comes about four times as often as the formula allows.

For random data, raw deflate at level 6 (the same as .NET Optimal) gives:

input actual output bound
280,224 B 280,314 B 280,313 B
1 MiB 1,048,896 B 1,048,725 B
10 MiB 10,488,960 B 10,486,629 B

Gzip and ZLib add their own wrapper on top of this, so they exceed the bound too.

Why it matters

ICompressionProvider.Compress(ReadOnlySpan<byte>) (Essentials/ICompressionProvider.cs:~55) allocates its buffer from GetMaxCompressedLength through ProviderHelpers.ExecuteToExactArray. For inputs above about 280 KB, TryCompress then returns false, and the caller gets InvalidOperationException("Compression failed to produce output with the allocated buffer."). This affects any already-compressed, encrypted or random payload, such as images, archives or ciphertext.

It also breaks the contract that a destination sized from GetMaxCompressedLength guarantees TryCompress succeeds.

ProviderContractTests.Compression_Bound_Holds_For_Incompressible_Input only exercises 4096 bytes, which is why it still passes.

Suggested fix

  • Use zlib's deflateBound: n + (n >> 12) + (n >> 14) + (n >> 25) + 13, plus the container overhead (+6 for zlib, +18 for gzip).
  • For Brotli, use BrotliEncoder.GetMaxCompressedLength(n).
  • Compute in long, and throw or clamp near int.MaxValue so the bound can't overflow.
  • Extend the contract test to 1 MiB and 10 MiB of random data for every provider.

Activity

  1. matt-edmondson commented on Sep 26, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    • Category: Bug
    • Priority: High. Compress(byte[]) throws for any incompressible payload above about 280 KB. That covers common inputs such as images, archives and ciphertext. It also breaks the GetMaxCompressedLength contract that a buffer of that size is always enough.
    • Area / suggested owner: Compression providers (Deflate, Gzip, ZLib, Brotli): GetMaxCompressedLength
    • Duplicates / in progress: None found. No open PR references this issue.
    • Notes: Use zlib's deflateBound plus the container overhead, and BrotliEncoder.GetMaxCompressedLength for Brotli. Extend the contract test beyond 4 KB, or it will keep passing.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions