Skip to content

AVRO-4324: [Java] Align ReflectDatumReader.readArray with GenericDatumReader eager-allocation guards - #3920

Merged
RyanSkraba merged 2 commits into
apache:mainfrom
iemejia:AVRO-4324-reflectdatumreader-readarray
Aug 6, 2026
Merged

AVRO-4324: [Java] Align ReflectDatumReader.readArray with GenericDatumReader eager-allocation guards#3920
RyanSkraba merged 2 commits into
apache:mainfrom
iemejia:AVRO-4324-reflectdatumreader-readarray

Conversation

@iemejia

@iemejia iemejia commented Aug 5, 2026

Copy link
Copy Markdown
Member

What changes were proposed in this pull request?

GenericDatumReader.readArray already 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.readArray override did not apply these guards: it called newArray(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 example long[]) could drive a large eager allocation before a single element was read.

This aligns ReflectDatumReader.readArray with the generic reader by applying the same ensureAvailableCollectionBytes and checkMaxCollectionAllocation guards before the allocation. Malformed input now fails fast (EOFException) instead of over-allocating; valid arrays read unchanged.

How was this patch tested?

  • New test in 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.
  • Full avro module test suite passes.

JIRA

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.
@github-actions github-actions Bot added the Java Pull Requests for Java binding label Aug 5, 2026
@iemejia
iemejia requested a lite review from Copilot August 6, 2026 09:10

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

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 ensureAvailableCollectionBytes and SystemLimitException.checkMaxCollectionAllocation (for zero-byte element schemas) before eager Java array allocation in ReflectDatumReader.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.

Comment on lines +154 to +157
ensureAvailableCollectionBytes(in, l, expectedType);
if (isZeroByteSchema(expectedType)) {
SystemLimitException.checkMaxCollectionAllocation(0, l);
}

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.

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)

@iemejia
iemejia requested a review from RyanSkraba August 6, 2026 09:25
…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.

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 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

  • readObjectArray grows primitive arrays via ArrayAccessor (using Arrays.copyOf), but object arrays never grow. If a valid Avro array is encoded in multiple blocks, the second block can make limit exceed array.length, leading to ArrayIndexOutOfBoundsException while decoding. Mirror the primitive-array path by expanding the array when limit grows 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;

@RyanSkraba
RyanSkraba merged commit 9b21002 into apache:main Aug 6, 2026
9 checks passed
RyanSkraba pushed a commit that referenced this pull request Aug 6, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Java Pull Requests for Java binding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants