Skip to content

GH-3712: Fail instead of writing CapacityByteArrayOutputStream content that does not match its size - #3965

Open
raviston wants to merge 1 commit into
apache:masterfrom
raviston:GH-3712-capacity-baos-consistency
Open

raviston wants to merge 1 commit into
apache:masterfrom
raviston:GH-3712-capacity-baos-consistency

Conversation

@raviston

Copy link
Copy Markdown

Rationale for this change

In the corrupt pages reported in #3712, the length prefix of the repetition levels and the page header both match the size() of the level stream, but the serialized payload is shorter. @edgarRd's analysis shows that a contiguous range ending exactly at a slab boundary of CapacityByteArrayOutputStream is missing. The analysis reproduces the stored page byte for byte when the position of an already full slab is lowered before serialization. size() comes from the independent bytesUsed counter, while writeTo() writes slab.position() bytes per slab, and nothing checks the two against each other. So the writer silently produced files that no reader can decode.

This PR does not identify what lowered the slab position. It makes that state fail the write instead of corrupting the file, and it removes the code paths in this class that change the position of a slab after it was written.

What changes are included in this PR?

  • writeTo and writeInto check that the slabs hold exactly size() bytes before they write anything, and throw an IllegalStateException otherwise. The check is one pass over the slab list.
  • writeInto and the direct buffer path of writeTo read the slabs through duplicate(). Before, they called flip() on the slabs themselves, so serializing changed the slabs' position and limit.
  • setByte used slab.limit() to find the slab holding a byte, which assumes that every slab but the last is full. Since writeInt/writeLong were added (GH-3530: Optimize PLAIN encoding and decoding with direct ByteBuffer I/O #3565), a slab can be followed by a new one before it is full, and then setByte patched the wrong byte. It now uses slab.position(). Today the only caller of setByte is RunLengthBitPackingHybridEncoder, which never calls writeInt/writeLong, so no current code path reaches this. It is fixed because it is the same kind of size/content mismatch.

Are these changes tested?

Yes. The new TestCapacityByteArrayOutputStream runs with heap and direct allocators:

The existing tests of parquet-common (346), parquet-encoding (34) and parquet-column (973) pass, as do the parquet-hadoop writer tests (TestParquetWriter*, TestColumnChunkPageWriteStore, TestParquetFileWriter, TestDataPageChecksums, TestStatistics, TestDirectCodecFactory).

Are there any user-facing changes?

A write that would have produced a corrupt page now fails with an IllegalStateException whose message gives both sizes.

Refs #3712. This does not close the issue, because the root cause is still unknown.

🤖 Generated with Claude Code

…content that does not match its size

The corrupt pages reported in apacheGH-3712 have a length prefix and a page header that
match the size() of the level stream, but a serialized payload that is shorter: a
full slab was serialized with a lower position than the bytes written to it. The
writer then silently produced a file that no reader can decode.

- writeTo and writeInto now check that the slabs hold exactly size() bytes and
  throw an IllegalStateException otherwise, so the write fails instead of
  producing a corrupt file.
- writeInto and the direct buffer path of writeTo read the slabs through
  duplicates, so serializing no longer changes the position and limit of the
  slabs.
- setByte located bytes by the limit of the slabs, assuming that every slab
  but the last is full. Since writeInt and writeLong were added, a slab may be
  followed by a new one before it is full, so setByte now uses the position.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants