GH-3716: Fix data corruption in ByteBufferBackedBinary.getBytes() for non-array-backed buffers - #3717
Conversation
|
Nice catch. +1 (non-binding). I confirmed the new test code fails under 1.18.0 |
Hi @dossett, thanks for the confirmation. I wish we could have a patch release for addressing this issue soon, since we are blocked by the Jackson CVEs which are resolved in 1.18.0. |
|
Out of curiosity, was this issue latent in the codebase, or was it introduced around the v1.18.0 changes?" |
|
Based on my analysis (with Codex) it was introduced in 1.18.0, specifically in #3565. I confirmed that 1.18.0 with that PR reverted will pass the new tests. |
|
@dossett Got it, thanks! So the bug in |
|
That's almost a philosophical question about what constitutes a bug :-) But there's definitely some truth to that. |
Fokko
left a comment
There was a problem hiding this comment.
Thanks for the quick follow up @yimingli-vmware 🙌
…y-backed buffers 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 <noreply@anthropic.com>
dd5f114 to
73cdcce
Compare
|
Hi @Fokko and @wgtmac, my PR got a failed workflow run since this Fix Maven parent and JUnit test compatibility merged into Master but my PR was in the stale state. I've already rebased my PR off Mater, could @wgtmac help me re-trigger my workflow to make sure this PR does not break anything? And @dossett created another PR which solves a similar issue, so may be that PR also needs to be added in the 1.18.1 milestone? Since i've got 2 approvals and it's my first PR to this project, should i merge this PR, or committers/members will merge this PR? Thanks! |
Rationale for this change
Fixes #3716.
FixedLenByteArrayPlainValuesReaderhands outBinaryvalues that allshare one page-wide
ByteBuffer, advancing its live position on everyreadBytes()call.Binary.ByteBufferBackedBinary.getBytes()andtoStringUsingUTF8()(non-array-backed branch) calledvalue.limit(offset + length)directly on that same shared buffer beforecapturing
value.position(). SinceByteBuffer.limit()clampspositiondown whenever
position > newLimit, callinggetBytes()on an earliervalue after later values have already advanced the buffer permanently
rewinds the buffer's live position -- corrupting every
readBytes()callthat follows.
This surfaces as data corruption when reading a repeated (
LIST)FIXED_LEN_BYTE_ARRAYcolumn with 2+ elements per row across 2+ rows:record assembly stores each
Binaryand only materializes it once a fullrow/group has been 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 (see #3716 for a
minimal standalone repro).
What changes are included in this PR?
Binary.ByteBufferBackedBinary.getBytes()and.toStringUsingUTF8()nowduplicate()the buffer before adjusting position/limit, so the sharedbuffer's own position is never mutated.
TestFixedLenByteArrayPlainValuesWriterReaderthat read values out ofthe order they were materialized, matching the lazy-consumption pattern
from record assembly, and fail against unpatched 1.18.0.
Are these changes tested?
Yes -- two new tests
(
testLazyGetBytesDoesNotCorruptSubsequentReadsDirectBuffer,testLazyToStringUsingUTF8DoesNotCorruptSubsequentReadsDirectBuffer) inparquet-column/src/test/java/org/apache/parquet/column/values/plain/TestFixedLenByteArrayPlainValuesWriterReader.javafail on the unpatched code and pass with this fix. Also verified against
the full-file-roundtrip repro from #3716.
Are there any user-facing changes?
No API changes. This fixes a silent data-corruption bug introduced in
1.18.0; no user-facing behavior changes other than correct results.