This is an automated email from the ASF dual-hosted git repository.

Fokko pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/parquet-java.git


The following commit(s) were added to refs/heads/master by this push:
     new e02f65e2b Fix data corruption in ByteBufferBackedBinary.getBytes() for 
non-array-backed buffers (#3717)
e02f65e2b is described below

commit e02f65e2ba41393f5d95f14d2ff27029f5c90639
Author: Yiming Li <[email protected]>
AuthorDate: Tue Aug 25 12:02:20 2026 -0700

    Fix data corruption in ByteBufferBackedBinary.getBytes() for 
non-array-backed buffers (#3717)
    
    FixedLenByteArrayPlainValuesReader hands out Binary values that all
    share one page-wide ByteBuffer, advancing its live position on every
    readBytes() call. getBytes() and toStringUsingUTF8() on the
    non-array-backed path called value.limit(offset + length) directly on
    that shared buffer before capturing position. ByteBuffer.limit()
    clamps position down whenever position > newLimit, so calling
    getBytes() on an earlier value after later values have already
    advanced the buffer permanently rewinds its live position -- corrupting
    every readBytes() call that follows.
    
    This surfaces as data corruption when reading a repeated
    (LIST) FIXED_LEN_BYTE_ARRAY column with 2+ elements per row across 2+
    rows: record assembly stores each Binary and only materializes it once
    a full row is built, which is exactly the lazy-after-later-value
    pattern that triggers the clamp. Each subsequent row reads back the
    previous row's last-written element instead of its own.
    
    Fix both methods to duplicate() the buffer before adjusting its
    position/limit, so the shared buffer's own position is never mutated.
    
    Co-authored-by: Claude Sonnet 5 <[email protected]>
---
 .../java/org/apache/parquet/io/api/Binary.java     | 37 +++++++------
 ...stFixedLenByteArrayPlainValuesWriterReader.java | 60 ++++++++++++++++++++++
 2 files changed, 80 insertions(+), 17 deletions(-)

diff --git a/parquet-column/src/main/java/org/apache/parquet/io/api/Binary.java 
b/parquet-column/src/main/java/org/apache/parquet/io/api/Binary.java
index 3160f091e..58961256f 100644
--- a/parquet-column/src/main/java/org/apache/parquet/io/api/Binary.java
+++ b/parquet-column/src/main/java/org/apache/parquet/io/api/Binary.java
@@ -442,16 +442,14 @@ public abstract class Binary implements 
Comparable<Binary>, Serializable {
       if (value.hasArray()) {
         ret = new String(value.array(), value.arrayOffset() + offset, length, 
StandardCharsets.UTF_8);
       } else {
-        int limit = value.limit();
-        value.limit(offset + length);
-        int position = value.position();
-        value.position(offset);
-        // no corresponding interface to read a subset of a buffer, would have 
to slice it
-        // which creates another ByteBuffer object or do what is done here to 
adjust the
-        // limit/offset and set them back after
-        ret = StandardCharsets.UTF_8.decode(value).toString();
-        value.limit(limit);
-        value.position(position);
+        // Duplicate before adjusting position/limit so we never mutate the 
shared
+        // buffer's own position: readBytes() may have already advanced it past
+        // this value's range (e.g. lazily-consumed values in a repeated 
field),
+        // and limit(offset + length) would otherwise silently clamp it 
backwards.
+        ByteBuffer duplicate = value.duplicate();
+        duplicate.position(offset);
+        duplicate.limit(offset + length);
+        ret = StandardCharsets.UTF_8.decode(duplicate).toString();
       }
 
       return ret;
@@ -475,13 +473,18 @@ public abstract class Binary implements 
Comparable<Binary>, Serializable {
     public byte[] getBytes() {
       byte[] bytes = new byte[length];
 
-      int limit = value.limit();
-      value.limit(offset + length);
-      int position = value.position();
-      value.position(offset);
-      value.get(bytes);
-      value.limit(limit);
-      value.position(position);
+      if (value.hasArray()) {
+        System.arraycopy(value.array(), value.arrayOffset() + offset, bytes, 
0, length);
+      } else {
+        // Duplicate before adjusting position/limit so we never mutate the 
shared
+        // buffer's own position: readBytes() may have already advanced it past
+        // this value's range (e.g. lazily-consumed values in a repeated 
field),
+        // and limit(offset + length) would otherwise silently clamp it 
backwards.
+        ByteBuffer duplicate = value.duplicate();
+        duplicate.position(offset);
+        duplicate.limit(offset + length);
+        duplicate.get(bytes);
+      }
       if (!isBackingBytesReused) { // backing buffer might change
         cachedBytes = bytes;
       }
diff --git 
a/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestFixedLenByteArrayPlainValuesWriterReader.java
 
b/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestFixedLenByteArrayPlainValuesWriterReader.java
index d86eefbe7..66db13c10 100644
--- 
a/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestFixedLenByteArrayPlainValuesWriterReader.java
+++ 
b/parquet-column/src/test/java/org/apache/parquet/column/values/plain/TestFixedLenByteArrayPlainValuesWriterReader.java
@@ -160,4 +160,64 @@ public class TestFixedLenByteArrayPlainValuesWriterReader {
       // Should not throw
     }
   }
+
+  // ---- Lazy getBytes() must not corrupt the shared page buffer's live 
position ----
+
+  // Regression test: getBytes() on an old value used to shift the buffer's 
shared
+  // read position backwards, so the next readBytes() call returned stale data.
+  @Test
+  public void testLazyGetBytesDoesNotCorruptSubsequentReadsDirectBuffer() 
throws IOException {
+    Binary[] expected = {fixedBinary(0), fixedBinary(50), fixedBinary(100), 
fixedBinary(150)};
+    ByteBuffer direct = writeToDirectBuffer(expected);
+
+    FixedLenByteArrayPlainValuesReader reader = new 
FixedLenByteArrayPlainValuesReader(FIXED_LEN);
+    reader.initFromPage(expected.length, ByteBufferInputStream.wrap(direct));
+
+    // "row 0": read two values, then materialize them lazily -- getBytes() on 
the
+    // first value happens after the shared buffer's position has already 
moved past it.
+    Binary row0v0 = reader.readBytes();
+    Binary row0v1 = reader.readBytes();
+    assertThat(row0v0.getBytes()).as("row0 value 
0").isEqualTo(expected[0].getBytes());
+    assertThat(row0v1.getBytes()).as("row0 value 
1").isEqualTo(expected[1].getBytes());
+
+    // "row 1": the corruption from materializing row 0 above must not affect 
this.
+    Binary row1v0 = reader.readBytes();
+    Binary row1v1 = reader.readBytes();
+    assertThat(row1v0.getBytes()).as("row1 value 
0").isEqualTo(expected[2].getBytes());
+    assertThat(row1v1.getBytes()).as("row1 value 
1").isEqualTo(expected[3].getBytes());
+  }
+
+  // Same scenario using toStringUsingUTF8(), which shares the buggy 
non-array-backed
+  // path with getBytes().
+  @Test
+  public void 
testLazyToStringUsingUTF8DoesNotCorruptSubsequentReadsDirectBuffer() throws 
IOException {
+    Binary[] expected = {fixedBinary(0), fixedBinary(50), fixedBinary(100), 
fixedBinary(150)};
+    ByteBuffer direct = writeToDirectBuffer(expected);
+
+    FixedLenByteArrayPlainValuesReader reader = new 
FixedLenByteArrayPlainValuesReader(FIXED_LEN);
+    reader.initFromPage(expected.length, ByteBufferInputStream.wrap(direct));
+
+    Binary row0v0 = reader.readBytes();
+    Binary row0v1 = reader.readBytes();
+    assertThat(row0v0.toStringUsingUTF8()).as("row0 value 
0").isEqualTo(expected[0].toStringUsingUTF8());
+    assertThat(row0v1.toStringUsingUTF8()).as("row0 value 
1").isEqualTo(expected[1].toStringUsingUTF8());
+
+    Binary row1v0 = reader.readBytes();
+    Binary row1v1 = reader.readBytes();
+    assertThat(row1v0.getBytes()).as("row1 value 
0").isEqualTo(expected[2].getBytes());
+    assertThat(row1v1.getBytes()).as("row1 value 
1").isEqualTo(expected[3].getBytes());
+  }
+
+  private ByteBuffer writeToDirectBuffer(Binary[] values) throws IOException {
+    try (FixedLenByteArrayPlainValuesWriter writer = newWriter()) {
+      for (Binary v : values) {
+        writer.writeBytes(v);
+      }
+      byte[] pageBytes = writer.getBytes().toByteArray();
+      ByteBuffer direct = ByteBuffer.allocateDirect(pageBytes.length);
+      direct.put(pageBytes);
+      direct.flip();
+      return direct;
+    }
+  }
 }

Reply via email to