This is an automated email from the ASF dual-hosted git repository.
RyanSkraba pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro.git
The following commit(s) were added to refs/heads/main by this push:
new 23aad5216f AVRO-4323: [Java] Bound DataFileStream block size against
available input before allocating the block buffer (#3919)
23aad5216f is described below
commit 23aad5216f874d0b7f3f2f43826cfb336747a970
Author: Ismaël Mejía <[email protected]>
AuthorDate: Thu Aug 6 17:52:46 2026 +0200
AVRO-4323: [Java] Bound DataFileStream block size against available input
before allocating the block buffer (#3919)
* AVRO-4323: [Java] Bound DataFileStream block size against available input
When reading a data (container) file, DataFileStream validated the declared
block size only against the Integer range before allocating the block
buffer.
For a malformed, corrupted, or truncated file the declared size can greatly
exceed the bytes actually present, so the reader eagerly allocated a large
buffer before reading any block byte.
Reject a declared block size that exceeds the number of bytes remaining in
the
input when that count is known (byte-array- or known-length-stream-backed
decoders), so reading a malformed file fails fast with a clear IOException.
The check is skipped when the remaining count is unknown (-1).
* AVRO-4323: Address review: account for sync marker in block-size guard;
close reader in test
Include the trailing sync-marker length (DataFileConstants.SYNC_SIZE) in the
remaining-bytes check so a file truncated right before the sync marker is
rejected before allocating the block buffer, and use try-with-resources in
the
regression test.
---
.../java/org/apache/avro/file/DataFileStream.java | 14 +++++
.../java/org/apache/avro/TestDataFileReader.java | 60 ++++++++++++++++++++++
2 files changed, 74 insertions(+)
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
index 9bb15183ec..fe95c76a15 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/file/DataFileStream.java
@@ -320,6 +320,20 @@ public class DataFileStream<D> implements Iterator<D>,
Iterable<D>, Closeable {
if (blockSize > Integer.MAX_VALUE || blockSize < 0) {
throw new IOException("Block size invalid or too large for this
implementation: " + blockSize);
}
+ // When the number of bytes remaining in the input is known (e.g. a
+ // byte-array- or known-length-stream-backed decoder), reject a declared
+ // block size that could not possibly be satisfied by the data available.
+ // Reading a block consumes the block bytes plus the trailing sync
marker,
+ // so both must fit in the remaining input. This avoids eagerly
allocating
+ // a large block buffer (see the DataBlock constructor) for a malformed,
+ // corrupted, or truncated file before any block byte has been read. A
+ // value of -1 means the remaining count is unknown, in which case the
+ // check is skipped.
+ int remaining = vin.remainingBytes();
+ if (remaining >= 0 && blockSize > (long) remaining -
DataFileConstants.SYNC_SIZE) {
+ throw new IOException("Block size " + blockSize + " plus sync marker
exceeds the number of bytes remaining in "
+ + "the input (" + remaining + "). The file is likely corrupted or
truncated.");
+ }
blockCount = blockRemaining;
availableBlock = true;
return true;
diff --git
a/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
b/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
index 0e13c8388c..a0f3fa5562 100644
--- a/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
+++ b/lang/java/avro/src/test/java/org/apache/avro/TestDataFileReader.java
@@ -319,4 +319,64 @@ public class TestDataFileReader {
return output.toByteArray();
}
+
+ /**
+ * A block header may declare a block size much larger than the data actually
+ * present in a corrupted or truncated file. When the remaining byte count is
+ * known, the reader must reject such a block up front rather than
attempting to
+ * allocate a buffer of the declared size.
+ */
+ @Test
+ void oversizedBlockSizeIsRejectedBeforeAllocation() throws IOException {
+ Schema schema = new Schema.Parser().parse("{\"type\":\"int\"}");
+
+ // A spec-correct header with zero records, so no real data block is
written.
+ ByteArrayOutputStream fileBytes = new ByteArrayOutputStream();
+ try (DataFileWriter<Object> w = new DataFileWriter<>(new
GenericDatumWriter<>(schema))) {
+ w.create(schema, fileBytes);
+ }
+
+ // Append a single block header that declares a huge block size but no
data.
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(fileBytes,
null);
+ encoder.writeLong(1L); // block entry count
+ encoder.writeLong(2_000_000_000L); // block size in bytes, far larger than
what follows
+ encoder.flush();
+
+ byte[] malformed = fileBytes.toByteArray();
+
+ // hasNextBlock() surfaces block-header IOExceptions wrapped in an
+ // AvroRuntimeException, the same way the existing "block size too large"
+ // check does.
+ AvroRuntimeException exception = assertThrows(AvroRuntimeException.class,
() -> {
+ try (DataFileStream<Object> reader = new DataFileStream<>(new
ByteArrayInputStream(malformed),
+ new GenericDatumReader<>())) {
+ while (reader.hasNext()) {
+ reader.next();
+ }
+ }
+ });
+ assertNotNull(exception.getMessage());
+ assertTrue(exception.getMessage().contains("Block size"), "Unexpected
message: " + exception.getMessage());
+ }
+
+ /**
+ * A valid single-record file must still read normally after the guard is
added.
+ */
+ @Test
+ void validFileWithSingleRecordStillReads() throws IOException {
+ Schema schema = new Schema.Parser().parse("{\"type\":\"int\"}");
+
+ ByteArrayOutputStream fileBytes = new ByteArrayOutputStream();
+ try (DataFileWriter<Object> w = new DataFileWriter<>(new
GenericDatumWriter<>(schema))) {
+ w.create(schema, fileBytes);
+ w.append(42);
+ }
+
+ try (DataFileStream<Object> reader = new DataFileStream<>(new
ByteArrayInputStream(fileBytes.toByteArray()),
+ new GenericDatumReader<>())) {
+ assertTrue(reader.hasNext());
+ assertEquals(42, reader.next());
+ assertFalse(reader.hasNext());
+ }
+ }
}