Skip to content

AVRO-4286: [csharp] Enforce a maximum decompressed block size#3857

Open
iemejia wants to merge 5 commits into
apache:mainfrom
iemejia:AVRO-4286-csharp-decompress-limit
Open

AVRO-4286: [csharp] Enforce a maximum decompressed block size#3857
iemejia wants to merge 5 commits into
apache:mainfrom
iemejia:AVRO-4286-csharp-decompress-limit

Conversation

@iemejia

@iemejia iemejia commented Jul 11, 2026

Copy link
Copy Markdown
Member

What is the purpose of the change

The built-in deflate codec is inflated in chunks and bounded before the full output is materialized, and DataFileReader additionally checks the decompressed size of every codec's output as a safeguard. Exceeding the limit throws AvroRuntimeException.

When reading a data file, each block is decompressed according to the file's codec. A block with a very high compression ratio (or a malformed block) could expand to far more memory than its compressed size (a decompression bomb). This enforces a configurable maximum decompressed size while reading each block, mirroring the Java SDK's decompression limit (AVRO-4247). The limit defaults to 200 MiB and can be overridden with the AVRO_MAX_DECOMPRESS_LENGTH environment variable.

This is part of the umbrella issue AVRO-4283.

Verifying this change

This change added tests and can be verified as follows:

  • Added tests in test/File/FileTests.cs: a deflate block exceeding the limit is rejected and a within-limit block decodes.
  • Run: cd lang/csharp && dotnet test src/apache/test/Avro.test.csproj

Documentation

  • Does this pull request introduce a new feature? (no — hardening/robustness)
  • If yes, how is the feature documented? (not applicable; the new AVRO_MAX_DECOMPRESS_LENGTH environment variable is documented in code comments)

When reading a data file, each block is decompressed according to the
file's codec. A block with a very high compression ratio (or a malformed
block) could expand to far more memory than its compressed size. Enforce a
configurable maximum decompressed size, mirroring the Java SDK's
decompression limit (AVRO-4247): the built-in deflate codec is inflated in
chunks and bounded before the full output is materialized, and DataFileReader
additionally checks the decompressed size of every codec's output as a
safeguard. The limit defaults to 200 MiB and can be overridden with the
AVRO_MAX_DECOMPRESS_LENGTH environment variable; exceeding it throws
AvroRuntimeException.

Assisted-by: GitHub Copilot:claude-opus-4.8

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the C# Avro data-file reader against decompression bombs by enforcing a configurable maximum decompressed block size (default 200 MiB), aligned with similar protections in other SDKs.

Changes:

  • Add a global maximum decompressed block size (AVRO_MAX_DECOMPRESS_LENGTH) and bounded stream-copy helper in Codec.
  • Enforce the limit during deflate decompression and add a post-decompression safeguard in DataFileReader.
  • Add NUnit coverage for both exceeding-limit and within-limit deflate blocks.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
lang/csharp/src/apache/test/File/FileTests.cs Adds tests validating deflate blocks are rejected/accepted based on the configured decompression limit.
lang/csharp/src/apache/main/File/DeflateCodec.cs Switches deflate decompression to a bounded copy to fail early when output would exceed the max size.
lang/csharp/src/apache/main/File/DataFileReader.cs Adds a post-decompression size check as a safeguard for codecs that return fully materialized buffers.
lang/csharp/src/apache/main/File/Codec.cs Introduces default limit/env override + helpers for checking and bounded copying.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +71 to +73
throw new AvroRuntimeException(
$"Decompressed block size exceeds the maximum allowed of {maxLength} bytes");
}

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.

Fixed in 15737e5 — the exception now reports the observed decompressed size and names the AVRO_MAX_DECOMPRESS_LENGTH environment variable to raise the limit.

Comment on lines +84 to +89
public static void CopyBounded(Stream source, Stream destination, long maxLength)
{
byte[] buffer = new byte[81920];
long total = 0;
int read;
while ((read = source.Read(buffer, 0, buffer.Length)) > 0)

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.

Fixed in 15737e5 — CopyBounded now validates its arguments (ArgumentNullException for null streams, ArgumentOutOfRangeException for a negative maxLength), with a test.

…Bounded args

- The decompression-limit exception now reports the observed decompressed size
  and names the AVRO_MAX_DECOMPRESS_LENGTH environment variable to raise it.
- CopyBounded (public API) validates its arguments, throwing ArgumentNullException
  for null streams and ArgumentOutOfRangeException for a negative maxLength
  instead of a NullReferenceException / unexpected behavior. Add a test.

Assisted-by: GitHub Copilot:claude-opus-4.8

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines 337 to +342
_currentBlock.Data = _codec.Decompress(_currentBlock.Data, (int)_blockSize);
// Guard against a block that decompresses to more than the
// allowed maximum (a decompression bomb). The built-in deflate
// codec is already bounded during decompression; this covers
// any codec that returns a fully decompressed buffer.
Codec.CheckDecompressLength(_currentBlock.Data.Length, Codec.GetMaxDecompressLength());

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.

Fixed. HasNext() now passes the original exception as the inner exception (and uses e.Message in the wrapper text), so the underlying cause and stack trace are preserved for callers. Pushed in f8c8e1d.

HasNext() wrapped any block-fetch failure in an AvroRuntimeException but only
folded the original into the message string, dropping the stack trace and
exception chain. Pass the original as the inner exception and use its Message
in the text so callers can inspect and log the underlying cause.

Assisted-by: GitHub Copilot:claude-opus-4.8

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment on lines 233 to 237
catch (Exception e)
{
throw new AvroRuntimeException(string.Format(CultureInfo.InvariantCulture,
"Error fetching meta data for key: {0}", key), e);
"Error fetching next object from block: {0}", e.Message), e);
}

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.

Fixed. My earlier change had accidentally edited GetMetaString's catch. Restored its metadata-specific message ("Error fetching meta data for key: {key}") with the inner exception preserved. Pushed in 7a05ac1.

Comment on lines 337 to +342
_currentBlock.Data = _codec.Decompress(_currentBlock.Data, (int)_blockSize);
// Guard against a block that decompresses to more than the
// allowed maximum (a decompression bomb). The built-in deflate
// codec is already bounded during decompression; this covers
// any codec that returns a fully decompressed buffer.
Codec.CheckDecompressLength(_currentBlock.Data.Length, Codec.GetMaxDecompressLength());

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.

Fixed. The inner-exception preservation is now applied to HasNext() (the intended target): it passes the original exception as InnerException and uses e.Message in the wrapper text. Pushed in 7a05ac1.

Comment on lines +338 to +342
// Guard against a block that decompresses to more than the
// allowed maximum (a decompression bomb). The built-in deflate
// codec is already bounded during decompression; this covers
// any codec that returns a fully decompressed buffer.
Codec.CheckDecompressLength(_currentBlock.Data.Length, Codec.GetMaxDecompressLength());

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.

Fixed. Added TestReaderRejectsOversizedBlock, which writes a Null-codec block larger than the limit and asserts the DataFileReader read path (HasNext -> CheckDecompressLength) rejects it, covering codecs that return a fully materialized buffer. Pushed in 7a05ac1.

…test reader path

- The previous change accidentally edited GetMetaString's catch instead of
  HasNext. Restore GetMetaString's metadata-specific message (with the key) and
  apply the inner-exception preservation to HasNext, where a block-fetch failure
  is wrapped: it now passes the original exception as InnerException and uses its
  Message in the text.
- Add TestReaderRejectsOversizedBlock, which writes a Null-codec block larger
  than the limit and asserts the DataFileReader read path (not just a direct
  codec call) rejects it, covering CheckDecompressLength for codecs that return a
  fully materialized buffer.

Assisted-by: GitHub Copilot:claude-opus-4.8
Comment thread lang/csharp/src/apache/test/File/FileTests.cs Fixed
Comment thread lang/csharp/src/apache/test/File/FileTests.cs Fixed
The oversized-block test had an empty foreach body with an unused loop
variable, which CodeQL flagged as a useless assignment and an empty loop body.
Assert.NotNull(rec) inside the loop reads the variable and gives the body a
meaningful statement while still forcing the block to be read and decompressed.

Assisted-by: GitHub Copilot:claude-opus-4.8

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants