Skip to content

Commit aaff5db

Browse files
authored
GH-3489: Fix Binary.copy() for direct ByteBuffer-backed values (#3490)
Why Binary.copy() is a no-op for ByteBufferBackedBinary instances when isBackingBytesReused is false, returning this instead of a heap-backed copy. When the off-heap decompression path is enabled, column readers produce ByteBufferBackedBinary values that are zero-copy views into Arena-managed direct buffers. Code that calls copy() to take ownership of the data -- such as DictionaryValuesWriter.writeBytes() -- silently receives the original object still pointing into the direct buffer. When the source PageReadStore is closed and the Arena is freed, those Binary values become dangling references, causing IllegalStateException: Already closed on subsequent access. What Override copy() in Binary.ByteBufferBackedBinary to always materialize to a heap-backed Binary via fromConstantByteArray(getBytes()) when the backing ByteBuffer is direct. Heap-backed ByteBuffer values delegate to the existing super.copy() behavior to avoid unnecessary copies. The change is in parquet-column/src/main/java/org/apache/parquet/io/api/Binary.java.
1 parent f280d55 commit aaff5db

2 files changed

Lines changed: 147 additions & 0 deletions

File tree

parquet-column/src/main/java/org/apache/parquet/io/api/Binary.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,16 @@ public Binary slice(int start, int length) {
498498
return Binary.fromConstantByteArray(getBytesUnsafe(), start, length);
499499
}
500500

501+
@Override
502+
public Binary copy() {
503+
if (value.isDirect()) {
504+
// Direct ByteBuffers may be backed by memory that can be freed independently, so always materialize to
505+
// a heap-backed copy to avoid use-after-free.
506+
return Binary.fromConstantByteArray(getBytes());
507+
}
508+
return super.copy();
509+
}
510+
501511
@Override
502512
public int hashCode() {
503513
if (value.hasArray()) {

parquet-column/src/test/java/org/apache/parquet/io/api/TestBinary.java

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import static org.junit.Assert.assertArrayEquals;
2222
import static org.junit.Assert.assertEquals;
23+
import static org.junit.Assert.assertNotSame;
2324
import static org.junit.Assert.assertSame;
2425
import static org.junit.Assert.assertTrue;
2526
import static org.junit.Assert.fail;
@@ -109,6 +110,27 @@ public BinaryAndOriginal get(byte[] bytes, boolean reused) throws Exception {
109110
}
110111
};
111112

113+
private static final BinaryFactory DIRECT_BUFFER_BF = new BinaryFactory() {
114+
@Override
115+
public BinaryAndOriginal get(byte[] bytes, boolean reused) throws Exception {
116+
ByteBuffer direct = ByteBuffer.allocateDirect(bytes.length);
117+
direct.put(bytes);
118+
direct.flip();
119+
Binary b;
120+
121+
if (reused) {
122+
b = Binary.fromReusedByteBuffer(direct);
123+
} else {
124+
b = Binary.fromConstantByteBuffer(direct);
125+
}
126+
127+
assertArrayEquals(bytes, b.getBytes());
128+
// Return the backing byte[] so tests can mutate it, though for direct buffers
129+
// there is no accessible backing array. We return a copy of the original bytes.
130+
return new BinaryAndOriginal(b, bytes);
131+
}
132+
};
133+
112134
private static final BinaryFactory STRING_BF = new BinaryFactory() {
113135
@Override
114136
public BinaryAndOriginal get(byte[] bytes, boolean reused) throws Exception {
@@ -151,6 +173,71 @@ public void testByteBufferBackedBinary() throws Exception {
151173
testBinary(BUFFER_BF, false);
152174
}
153175

176+
@Test
177+
public void testDirectByteBufferBackedBinary() throws Exception {
178+
// Direct buffers have different copy() semantics (always materializes to heap),
179+
// so we test them separately instead of using the generic testBinary flow.
180+
testSlice(DIRECT_BUFFER_BF, true);
181+
testSlice(DIRECT_BUFFER_BF, false);
182+
testDirectConstantCopy(DIRECT_BUFFER_BF);
183+
testDirectReusedCopy(DIRECT_BUFFER_BF);
184+
testSerializable(DIRECT_BUFFER_BF, true);
185+
testSerializable(DIRECT_BUFFER_BF, false);
186+
}
187+
188+
@Test
189+
public void testDirectByteBufferCopyAlwaysMaterializesToHeap() throws Exception {
190+
// For constant (non-reused) direct ByteBuffers, copy() must return a new Binary
191+
// rather than 'this', because the direct memory can be freed independently.
192+
byte[] data = testString.getBytes(UTF8);
193+
ByteBuffer direct = ByteBuffer.allocateDirect(data.length);
194+
direct.put(data);
195+
direct.flip();
196+
197+
Binary binary = Binary.fromConstantByteBuffer(direct);
198+
Binary copy = binary.copy();
199+
200+
// The copy must NOT be the same object, even though the binary is constant
201+
assertNotSame("copy() of a direct ByteBuffer-backed constant Binary must not return 'this'", binary, copy);
202+
assertArrayEquals(data, copy.getBytes());
203+
assertArrayEquals(data, copy.getBytesUnsafe());
204+
}
205+
206+
@Test
207+
public void testDirectByteBufferCopyIsIndependentOfOriginalBuffer() throws Exception {
208+
// Verify the copied Binary is independent of the original direct ByteBuffer.
209+
// Simulates the scenario where direct memory is overwritten after copy.
210+
byte[] data = testString.getBytes(UTF8);
211+
ByteBuffer direct = ByteBuffer.allocateDirect(data.length);
212+
direct.put(data);
213+
direct.flip();
214+
215+
Binary binary = Binary.fromReusedByteBuffer(direct);
216+
Binary copy = binary.copy();
217+
218+
// Overwrite the direct buffer content to simulate memory reuse / free
219+
direct.clear();
220+
for (int i = 0; i < data.length; i++) {
221+
direct.put((byte) 0);
222+
}
223+
224+
// The copy should still hold the original data
225+
assertArrayEquals(data, copy.getBytes());
226+
assertArrayEquals(data, copy.getBytesUnsafe());
227+
}
228+
229+
@Test
230+
public void testHeapByteBufferConstantCopyReturnsSame() throws Exception {
231+
// For heap-backed constant ByteBuffers, copy() should return 'this' (existing behavior)
232+
byte[] data = testString.getBytes(UTF8);
233+
ByteBuffer heap = ByteBuffer.wrap(data);
234+
235+
Binary binary = Binary.fromConstantByteBuffer(heap);
236+
Binary copy = binary.copy();
237+
238+
assertSame("copy() of a heap ByteBuffer-backed constant Binary should return 'this'", binary, copy);
239+
}
240+
154241
@Test
155242
public void testEqualityMethods() throws Exception {
156243
Binary bin1 = Binary.fromConstantByteArray("alice".getBytes(), 1, 3);
@@ -226,6 +313,56 @@ private void testReusedCopy(BinaryFactory bf) throws Exception {
226313
assertArrayEquals(testString.getBytes(UTF8), copy.copy().getBytes());
227314
}
228315

316+
/**
317+
* Tests copy() on a constant (non-reused) direct ByteBuffer-backed Binary.
318+
* Unlike heap-backed binaries, copy() must return a new object because the direct
319+
* memory can be freed independently.
320+
*/
321+
private void testDirectConstantCopy(BinaryFactory bf) throws Exception {
322+
BinaryAndOriginal bao = bf.get(testString.getBytes(UTF8), false);
323+
assertEquals(false, bao.binary.isBackingBytesReused());
324+
325+
assertArrayEquals(testString.getBytes(UTF8), bao.binary.getBytes());
326+
assertArrayEquals(testString.getBytes(UTF8), bao.binary.getBytesUnsafe());
327+
assertArrayEquals(testString.getBytes(UTF8), bao.binary.copy().getBytesUnsafe());
328+
assertArrayEquals(testString.getBytes(UTF8), bao.binary.copy().getBytes());
329+
330+
bao = bf.get(testString.getBytes(UTF8), false);
331+
assertEquals(false, bao.binary.isBackingBytesReused());
332+
333+
Binary copy = bao.binary.copy();
334+
335+
// Direct ByteBuffer-backed constant Binary.copy() must NOT return 'this'
336+
assertNotSame(copy, bao.binary);
337+
// But the data must be equal
338+
assertEquals(bao.binary, copy);
339+
}
340+
341+
/**
342+
* Tests copy() on a reused direct ByteBuffer-backed Binary.
343+
* The copy must be fully independent and survive mutation of the original buffer.
344+
*/
345+
private void testDirectReusedCopy(BinaryFactory bf) throws Exception {
346+
BinaryAndOriginal bao = bf.get(testString.getBytes(UTF8), true);
347+
assertEquals(true, bao.binary.isBackingBytesReused());
348+
349+
assertArrayEquals(testString.getBytes(UTF8), bao.binary.getBytes());
350+
assertArrayEquals(testString.getBytes(UTF8), bao.binary.getBytesUnsafe());
351+
assertArrayEquals(testString.getBytes(UTF8), bao.binary.copy().getBytesUnsafe());
352+
assertArrayEquals(testString.getBytes(UTF8), bao.binary.copy().getBytes());
353+
354+
bao = bf.get(testString.getBytes(UTF8), true);
355+
assertEquals(true, bao.binary.isBackingBytesReused());
356+
357+
Binary copy = bao.binary.copy();
358+
assertNotSame(copy, bao.binary);
359+
360+
assertArrayEquals(testString.getBytes(UTF8), copy.getBytes());
361+
assertArrayEquals(testString.getBytes(UTF8), copy.getBytesUnsafe());
362+
assertArrayEquals(testString.getBytes(UTF8), copy.copy().getBytesUnsafe());
363+
assertArrayEquals(testString.getBytes(UTF8), copy.copy().getBytes());
364+
}
365+
229366
private void testSerializable(BinaryFactory bf, boolean reused) throws Exception {
230367
BinaryAndOriginal bao = bf.get("polygon".getBytes(UTF8), reused);
231368

0 commit comments

Comments
 (0)