iemejia opened a new pull request, #3865:
URL: https://github.com/apache/avro/pull/3865

   ## What is the purpose of the change
   
   Fixes AVRO-4300 (sub-task of AVRO-4277). When `GenericDatumReader` decodes 
an array it reads the block item count from the stream and pre-allocates the 
backing store (`newArray` → `new Object[count]`) before decoding any element. 
Two independent gaps let a tiny payload drive an unbounded allocation and 
exhaust the heap:
   
   1. **Zero-byte elements (classic and fast reader).** Elements whose schema 
encodes to zero bytes (`null`, a zero-length `fixed`, or a record with only 
zero-byte fields) consume no input, so `ensureAvailableCollectionBytes` 
(AVRO-4241) skips the check for them (`minBytesPerElement == 0`), and the 
collection-length cap is `Integer.MAX_VALUE - 8` (a JVM array-size ceiling, not 
a memory budget). An array such as `{"type":"array","items":"null"}` declaring 
a block count of 200,000,000 is a ~6-byte payload that allocates a 200M-slot 
array (~1.6 GB).
   
   2. **The fast reader never had the AVRO-4241 available-bytes guard at all.** 
That change only modified the classic `GenericDatumReader` (and 
`BinaryDecoder`/`Decoder`/`ValidatingDecoder`); it never touched 
`FastReaderBuilder`, which is the default decode path (`avro.io.fastread` 
defaults to `true`). So on the default reader even a non-zero-byte array such 
as `array<long>` or `array<int>` with a huge block count and no data 
pre-allocated `new GenericData.Array<>((int) count)` and exhausted the heap. 
Only the classic reader was protected.
   
   ### Fix (applied identically on the classic and fast reader paths)
   
   - Add `SystemLimitException.checkMaxCollectionAllocation`, a heap-aware 
cumulative cap for zero-byte elements (default `maxMemory()/4/8` elements, 
overridable via the `org.apache.avro.limits.collectionItems.maxAllocation` 
system property, mirroring the existing decompression limit).
   - Expose `GenericDatumReader.ensureAvailableCollectionBytes` and apply it, 
together with the zero-byte cap, before allocating each array block in 
`FastReaderBuilder`, so the fast and classic readers enforce identical guards.
   - Maps were already safe on both paths because each entry carries a string 
key of at least one byte (`ensureAvailableMapBytes` on the classic path; key 
reads consume bytes on the fast path).
   
   ## Verifying this change
   
   This change added tests and can be verified as follows:
   
   - Added `SystemLimitException.checkMaxCollectionAllocation` tests 
(single/cumulative/negative/overflow and heap-derived default).
   - Added a full matrix test asserting every collection kind is rejected 
(never OOM) with a huge block count and no data on **both** the fast (default) 
and classic readers: `array<null>` → `SystemLimitException`; `array<long>`, 
`array<int>`, `map<null>`, `map<long>` → `EOFException` (the full 5×2 set of 
combinations), plus a cumulative multi-block null case and a positive 
within-limit decode.
   - Manually verified against a PoC (`array<null>`, block count 200,000,000) 
under `-Xmx256m`: rejected with a clean `SystemLimitException` (no allocation) 
instead of `OutOfMemoryError`, on both reader paths; legitimate collections 
within the limit still decode.
   - `mvn -pl avro test` for the `generic` and `io` packages passes (no 
regressions); Spotless and Checkstyle are clean.
   
   ## Documentation
   
   - Does this pull request introduce a new feature? no
   - If yes, how is the feature documented? not applicable (adds the 
`org.apache.avro.limits.collectionItems.maxAllocation` system property, 
documented in `SystemLimitException` JavaDoc alongside the existing limit 
properties)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to