AVRO-4324: [Java] Align ReflectDatumReader.readArray with GenericDatumReader eager-allocation guards - #3920
Conversation
ReflectDatumReader.readArray allocated the backing Java array for the declared array block count (Array.newInstance) before reading any element, unlike GenericDatumReader.readArray which already validates the count against the bytes remaining and caps element types whose minimum encoded size is zero. Apply the same guards (ensureAvailableCollectionBytes plus checkMaxCollectionAllocation for zero-byte element types) before the eager allocation, so a malformed or truncated record mapped to a Java array field (e.g. long[]) fails fast with an EOFException instead of over-allocating. Valid arrays continue to read unchanged.
There was a problem hiding this comment.
Pull request overview
Aligns the Java ReflectDatumReader.readArray behavior with GenericDatumReader to avoid large eager allocations on malformed/truncated inputs when decoding Avro arrays into Java array fields.
Changes:
- Add
ensureAvailableCollectionBytesandSystemLimitException.checkMaxCollectionAllocation(for zero-byte element schemas) before eager Java array allocation inReflectDatumReader.readArray. - Add a regression test ensuring oversized declared array counts fail fast with
EOFException.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectDatumReader.java | Adds pre-allocation guards to prevent large eager allocations from malformed/truncated array payloads. |
| lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectDatumReader.java | Adds a test that asserts oversized declared array counts are rejected early. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ensureAvailableCollectionBytes(in, l, expectedType); | ||
| if (isZeroByteSchema(expectedType)) { | ||
| SystemLimitException.checkMaxCollectionAllocation(0, l); | ||
| } |
There was a problem hiding this comment.
Good catch — fixed. I extracted a shared nextArrayBlock helper that re-applies ensureAvailableCollectionBytes and, for zero-byte element schemas, checkMaxCollectionAllocation(existing, l) after every in.arrayNext(), and wired it into both readObjectArray and readCollection while tracking the cumulative count. This mirrors GenericDatumReader.readArray so a large logical array split across blocks is now bounded cumulatively. Added a regression test (read_PojoWithZeroByteList_rejectsCumulativeCountAcrossBlocks) covering two blocks that individually pass but exceed the limit together. (fad19cd)
…locks The zero-byte allocation guard was only applied to the first array block, so a large logical array split across multiple blocks could pass the first check while the cumulative count still exceeded the heap-aware limit. Mirror GenericDatumReader by re-validating each continuation block (via a shared nextArrayBlock helper) against the bytes remaining and, for zero-byte element types, the cumulative allocation limit, in both readObjectArray and readCollection.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (1)
lang/java/avro/src/main/java/org/apache/avro/reflect/ReflectDatumReader.java:209
readObjectArraygrows primitive arrays viaArrayAccessor(usingArrays.copyOf), but object arrays never grow. If a valid Avro array is encoded in multiple blocks, the second block can makelimitexceedarray.length, leading toArrayIndexOutOfBoundsExceptionwhile decoding. Mirror the primitive-array path by expanding the array whenlimitgrows before writing elements (in both conversion and non-conversion branches).
do {
int limit = index + (int) l;
while (index < limit) {
Object element = readWithConversion(null, expectedType, logicalType, conversion, in);
array[index] = element;
…mReader eager-allocation guards (#3920) * AVRO-4324: [Java] Guard ReflectDatumReader.readArray eager allocation ReflectDatumReader.readArray allocated the backing Java array for the declared array block count (Array.newInstance) before reading any element, unlike GenericDatumReader.readArray which already validates the count against the bytes remaining and caps element types whose minimum encoded size is zero. Apply the same guards (ensureAvailableCollectionBytes plus checkMaxCollectionAllocation for zero-byte element types) before the eager allocation, so a malformed or truncated record mapped to a Java array field (e.g. long[]) fails fast with an EOFException instead of over-allocating. Valid arrays continue to read unchanged. * AVRO-4324: Address review: bound cumulative allocation across array blocks The zero-byte allocation guard was only applied to the first array block, so a large logical array split across multiple blocks could pass the first check while the cumulative count still exceeded the heap-aware limit. Mirror GenericDatumReader by re-validating each continuation block (via a shared nextArrayBlock helper) against the bytes remaining and, for zero-byte element types, the cumulative allocation limit, in both readObjectArray and readCollection.
What changes were proposed in this pull request?
GenericDatumReader.readArrayalready validates a declared array block count against the bytes actually remaining (ensureAvailableCollectionBytes) and applies a heap-aware bound for element types whose minimum encoded size is zero, before eagerly allocating the backing storage.The
ReflectDatumReader.readArrayoverride did not apply these guards: it callednewArray(old, (int) l, ...)→Array.newInstance(elementClass, count)using the declared block count directly, so a malformed or truncated record mapped to a Java array field (for examplelong[]) could drive a large eager allocation before a single element was read.This aligns
ReflectDatumReader.readArraywith the generic reader by applying the sameensureAvailableCollectionBytesandcheckMaxCollectionAllocationguards before the allocation. Malformed input now fails fast (EOFException) instead of over-allocating; valid arrays read unchanged.How was this patch tested?
TestReflectDatumReader:read_PojoWithArray_rejectsOversizedArrayCount— a record declaring a ~2e9 array block count with no elements now fails fast instead of attempting a large allocation. Existing round-trip tests act as negative controls.avromodule test suite passes.JIRA