[ 
https://issues.apache.org/jira/browse/AVRO-4283?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18095743#comment-18095743
 ] 

Ismaël Mejía commented on AVRO-4283:
------------------------------------

h3. Acceptance criteria / edge cases for the decompressed-size bound

While implementing the per-SDK subtasks, please ensure the following are 
covered (and verify the already-done Java (AVRO-4247) and Rust (avro-rs #582) 
impls satisfy them):

# *Enforce incrementally — never trust a declared/frame size.* snappy declares 
the uncompressed length up-front; zstd exposes ZSTD_getFrameContentSize 
(attacker-controlled, or ZSTD_CONTENTSIZE_UNKNOWN/ERROR); deflate/bzip2/xz have 
no reliable declared size. The bound must be enforced *during* the 
inflate/streaming decode loop, not by pre-trusting a header value — a "check 
declared size then allocate" implementation is itself bomb-able.
# *Boundary conditions.* decompressed size == limit passes, limit+1 fails; also 
handle size-0 and empty compressed blocks.
# *Snappy −4 CRC underflow.* Avro's snappy framing appends a 4-byte CRC; guard 
the length − 4 subtraction against underflow (cf. the BadSnappyLength / 
checked_sub(4) case fixed in avro-rs #582). Every SDK's snappy path needs this.
# *xz/lzma decoder memory (dictionary) limit.* This is separate from the 
output-size cap: a malicious xz header can request a huge dictionary and OOM 
the decoder before emitting any output. Set a decoder memlimit in addition to 
bounding output.
# *Default + config consistency.* Default the limit to 200MB in every SDK 
(mirroring Java's org.apache.avro.limits.decompress.maxLength) and expose an 
equivalent tunable. Agree on the knob name (e.g. env 
AVRO_MAX_DECOMPRESSED_LENGTH vs. a per-SDK property) and document it alongside 
AVRO_MAX_COLLECTION_ITEMS.
# *Error taxonomy.* Reject an over-limit block with a distinct, bounded error 
("exceeds decompressed limit") separate from "corrupt/truncated" — never a 
generic OOM or crash.
# *Bound the compressed block read too.* The data-file reader allocates the 
compressed block buffer from the header-declared block size (e.g. Java 
DataFileStream: new byte[blockSize] guarded only by <= Integer.MAX_VALUE; a 
few-byte file can force a ~2GB allocation). This must be bounded (against 
bytes-available, capped when unknown) in the same PR, or the bomb OOMs before 
decompression even runs. This also protects the null-codec case.
# *Per-block freeing.* Confirm decompressed blocks are released between 
iterations so the per-block cap is sufficient and no reader retains them.

Items 1–8 also serve as the regression-test matrix: each should have a test 
that fails with a bounded, well-defined error rather than exhausting memory.

> Enforce a maximum decompressed block size across language SDKs
> --------------------------------------------------------------
>
>                 Key: AVRO-4283
>                 URL: https://issues.apache.org/jira/browse/AVRO-4283
>             Project: Apache Avro
>          Issue Type: Improvement
>          Components: c, c++, csharp, javascript, perl, php, python, ruby
>    Affects Versions: 1.11.5, 1.12.1
>            Reporter: Ismaël Mejía
>            Assignee: Ismaël Mejía
>            Priority: Major
>             Fix For: 1.12.2
>
>
> When reading an Avro container (data) file, each block is decompressed 
> according to the file's codec (deflate, bzip2, snappy, xz, zstandard). 
> Several SDKs read the compressed block into memory and decompress into an 
> output buffer that grows (or is sized from an attacker-declared length) 
> without any upper bound. A block with a very high compression ratio can 
> therefore expand to an amount of memory far larger than the input, leading to 
> excessive allocation on malformed or hostile input.
> The Java SDK addressed this in AVRO-4247 by enforcing a maximum decompressed 
> size in every codec, defaulting to 200MB and configurable via 
> {{org.apache.avro.limits.decompress.maxLength}}. This umbrella tracks 
> bringing the C, C++, C#, JavaScript, Perl, PHP, Python, and Ruby SDKs in line 
> with that behavior so all SDKs bound decompression output consistently.
> h3. Proposed approach
> * Enforce a configurable maximum decompressed size while 
> inflating/decompressing each data-file block, across all codecs.
> * Reject a block whose decompressed output would exceed the limit with a 
> clear error instead of continuing to allocate.
> * Default the limit to a runtime-safe value; allow it to be tuned per SDK 
> (mirroring the Java property).
> * Add regression tests: a block that decompresses to more than the limit must 
> fail with a bounded, well-defined error rather than exhausting memory.
> An audit of the current decompression paths (for reference in the subtasks):
> * C: {{lang/c/src/codec.c}} (deflate doubles the output buffer; snappy 
> allocates the attacker-declared uncompressed length; lzma doubles), driven 
> from {{lang/c/src/datafile.c}}.
> * C++: {{lang/c++/impl/DataFile.cc}} (deflate/snappy) and 
> {{lang/c++/impl/ZstdDecompressWrapper.cc}} (zstd trusts the frame content 
> size). The existing {{BoundedInputStream}} bounds only the compressed input.
> * C#: {{lang/csharp/src/apache/main/File/DataFileReader.cs}} and the codec 
> classes (deflate/snappy/zstandard/bzip2/xz), all via {{Stream.CopyTo}} into 
> an unbounded {{MemoryStream}}.
> * JavaScript: {{lang/js/lib/files.js}} (deflate via {{zlib.inflateRaw}} with 
> no {{maxOutputLength}}).
> * Perl: {{lang/perl/lib/Avro/DataFileReader.pm}} (a {{block_max_size}} 
> attribute exists but is never enforced).
> * PHP: {{lang/php/lib/DataFile/AvroDataIOReader.php}} 
> (deflate/zstandard/snappy/bzip2).
> * Python: {{lang/py/avro/codecs.py}} (deflate/bzip2/snappy/zstandard).
> * Ruby: {{lang/ruby/lib/avro/data_file.rb}} (deflate/snappy/zstandard).
> Subtasks: one per SDK (below).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to