This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git
The following commit(s) were added to refs/heads/main by this push:
new cafb6601 fix(parquet): validate DataPageV2 decoded length (#911)
cafb6601 is described below
commit cafb6601743e08abfa7b456791bbefe1059aae03
Author: Minh Vu <[email protected]>
AuthorDate: Thu Jul 9 23:14:45 2026 +0200
fix(parquet): validate DataPageV2 decoded length (#911)
---
parquet/file/file_reader_test.go | 102 +++++++++++++++++++++++++++++++++++++++
parquet/file/page_reader.go | 85 +++++++++++++++++++++++---------
2 files changed, 163 insertions(+), 24 deletions(-)
diff --git a/parquet/file/file_reader_test.go b/parquet/file/file_reader_test.go
index c764ff07..5ae9ae74 100644
--- a/parquet/file/file_reader_test.go
+++ b/parquet/file/file_reader_test.go
@@ -215,6 +215,108 @@ func (p *PageSerdeSuite) TestDataPageV2() {
p.alloc.AssertSize(p.T(), 0)
}
+func (p *PageSerdeSuite) TestDataPageV2CorruptDecodedLength() {
+ rawData := bytes.Repeat([]byte{0x42}, 128)
+ codec, err := compress.GetCodec(compress.Codecs.Snappy)
+ p.NoError(err)
+
+ buffer := make([]byte, int(codec.CompressBound(int64(len(rawData)))))
+ compressed := codec.Encode(buffer, rawData)
+
+ p.dataPageHdrV2 = *format.NewDataPageHeaderV2()
+ p.dataPageHdrV2.NumValues = 1
+ p.dataPageHdrV2.NumRows = 1
+ p.dataPageHdrV2.Encoding = format.Encoding_PLAIN
+
+ p.WriteDataPageHeaderV2(1024, int32(len(rawData)+1),
int32(len(compressed)))
+ _, err = p.sink.Write(compressed)
+ p.NoError(err)
+
+ p.InitSerializedPageReader(1, compress.Codecs.Snappy)
+ p.False(p.pageReader.Next())
+ p.ErrorContains(p.pageReader.Err(), "metadata said 129 bytes
uncompressed data page, got 128 bytes")
+ p.NoError(p.pageReader.Close())
+ p.alloc.AssertSize(p.T(), 0)
+}
+
+func (p *PageSerdeSuite) TestDataPageV2Compressed() {
+ rawData := bytes.Repeat([]byte{0x42}, 128)
+ codec, err := compress.GetCodec(compress.Codecs.Snappy)
+ p.NoError(err)
+
+ buffer := make([]byte, int(codec.CompressBound(int64(len(rawData)))))
+ compressed := codec.Encode(buffer, rawData)
+
+ p.dataPageHdrV2 = *format.NewDataPageHeaderV2()
+ p.dataPageHdrV2.NumValues = 1
+ p.dataPageHdrV2.NumRows = 1
+ p.dataPageHdrV2.Encoding = format.Encoding_PLAIN
+
+ p.WriteDataPageHeaderV2(1024, int32(len(rawData)),
int32(len(compressed)))
+ _, err = p.sink.Write(compressed)
+ p.NoError(err)
+
+ p.InitSerializedPageReader(1, compress.Codecs.Snappy)
+ p.True(p.pageReader.Next())
+ p.Equal(rawData, p.pageReader.Page().Data())
+ p.NoError(p.pageReader.Close())
+ p.alloc.AssertSize(p.T(), 0)
+}
+
+type mockDecryptor struct {
+ decrypt func([]byte) []byte
+
+ aad []byte
+ mem memory.Allocator
+}
+
+func (m *mockDecryptor) FileAad() string { return "" }
+
+func (m *mockDecryptor) Allocator() memory.Allocator {
+ if m.mem == nil {
+ return memory.DefaultAllocator
+ }
+
+ return m.mem
+}
+
+func (m *mockDecryptor) CiphertextSizeDelta() int { return 0 }
+
+func (m *mockDecryptor) Decrypt(src []byte) []byte { return m.decrypt(src) }
+
+func (m *mockDecryptor) DecryptFrom(r io.Reader) []byte {
+ decrypted, _ := io.ReadAll(r)
+ return m.Decrypt(decrypted)
+}
+
+func (m *mockDecryptor) UpdateAad(aad string) { m.aad = []byte(aad) }
+
+func (p *PageSerdeSuite) TestDataPageV2EncryptedUncompressedLengthMismatch() {
+ p.dataPageHdrV2 = *format.NewDataPageHeaderV2()
+ p.dataPageHdrV2.NumValues = 1
+ p.dataPageHdrV2.NumRows = 1
+ p.dataPageHdrV2.Encoding = format.Encoding_PLAIN
+ p.dataPageHdrV2.IsCompressed = false
+
+ payload := bytes.Repeat([]byte{0xaa}, 4)
+ decryptedPayload := []byte{0xaa}
+
+ p.WriteDataPageHeaderV2(1024, int32(len(payload)), int32(len(payload)))
+ _, err := p.sink.Write(payload)
+ p.NoError(err)
+
+ p.EndStream()
+ p.alloc = memory.NewCheckedAllocator(memory.NewGoAllocator())
+ p.pageReader, _ =
file.NewPageReader(utils.NewByteReader(p.buffer.Bytes()), 1,
compress.Codecs.Uncompressed, p.alloc, &file.CryptoContext{
+ DataDecryptor: &mockDecryptor{decrypt: func([]byte) []byte {
return decryptedPayload }},
+ })
+
+ p.False(p.pageReader.Next())
+ p.ErrorContains(p.pageReader.Err(), "metadata said 4 bytes uncompressed
data page, got 1 bytes")
+ p.NoError(p.pageReader.Close())
+ p.alloc.AssertSize(p.T(), 0)
+}
+
func (p *PageSerdeSuite) TestLargePageHeaders() {
const (
statsSize = 256 * 1024 // 256KB
diff --git a/parquet/file/page_reader.go b/parquet/file/page_reader.go
index 8c6c6ace..66b8473a 100644
--- a/parquet/file/page_reader.go
+++ b/parquet/file/page_reader.go
@@ -518,53 +518,85 @@ func (p *serializedPageReader) decompress(rd io.Reader,
lenCompressed int, buf [
return p.codec.Decode(buf, data), nil
}
-func (p *serializedPageReader) readV2Encrypted(rd io.Reader, lenCompressed
int, levelsBytelen int, compressed bool, buf []byte) error {
+func (p *serializedPageReader) readV2Encrypted(rd io.Reader, lenCompressed
int, lenUncompressed int, levelsBytelen int, compressed bool, buf []byte) (int,
error) {
// if encrypted, we need to decrypt before decompressing
p.decompressBuffer.ResizeNoShrink(lenCompressed)
n, err := io.ReadFull(rd, p.decompressBuffer.Bytes()[:lenCompressed])
if err != nil {
- return err
+ return n, err
}
if n != lenCompressed {
- return fmt.Errorf("parquet: expected to read %d compressed
bytes, got %d", lenCompressed, n)
+ return n, fmt.Errorf("parquet: expected to read %d compressed
bytes, got %d", lenCompressed, n)
}
data :=
p.cryptoCtx.DataDecryptor.Decrypt(p.decompressBuffer.Bytes()[:lenCompressed])
+
// encrypted + uncompressed -> just copy the decrypted data to output
buffer
if !compressed {
- copy(buf, data)
- return nil
+ if len(data) != lenUncompressed {
+ return n, fmt.Errorf("parquet: metadata said %d bytes
uncompressed data page, got %d bytes", lenUncompressed, len(data))
+ }
+
+ copied := copy(buf, data)
+ if copied != lenUncompressed {
+ return copied, fmt.Errorf("parquet: expected to read %d
uncompressed encrypted bytes, got %d", lenUncompressed, copied)
+ }
+ return copied, nil
}
// definition + repetition levels are always uncompressed
if levelsBytelen > 0 {
- copy(buf, data[:levelsBytelen])
+ if len(data) < levelsBytelen {
+ return n, fmt.Errorf("parquet: expected to read %d
bytes for levels, got %d", levelsBytelen, len(data))
+ }
+ copied := copy(buf, data[:levelsBytelen])
+ if copied != levelsBytelen {
+ return copied, fmt.Errorf("parquet: expected to read %d
bytes for levels, got %d", levelsBytelen, copied)
+ }
data = data[levelsBytelen:]
}
- p.codec.Decode(buf[levelsBytelen:], data)
- return nil
+ decoded := p.codec.Decode(buf[levelsBytelen:], data)
+ if len(decoded) != lenUncompressed-levelsBytelen {
+ return levelsBytelen + len(decoded), fmt.Errorf("parquet:
metadata said %d bytes uncompressed data page, got %d bytes", lenUncompressed,
levelsBytelen+len(decoded))
+ }
+ return levelsBytelen + len(decoded), nil
}
-func (p *serializedPageReader) readV2Unencrypted(rd io.Reader, lenCompressed
int, levelsBytelen int, compressed bool, buf []byte) error {
+func (p *serializedPageReader) readV2Unencrypted(rd io.Reader, lenCompressed
int, lenUncompressed int, levelsBytelen int, compressed bool, buf []byte) (int,
error) {
if !compressed {
// uncompressed, just read into the buffer
- if _, err := io.ReadFull(rd, buf); err != nil {
- return err
+ n, err := io.ReadFull(rd, buf)
+ if err != nil {
+ return n, err
+ }
+ if n != lenUncompressed {
+ return n, fmt.Errorf("parquet: expected to read %d
uncompressed bytes, got %d", lenUncompressed, n)
}
- return nil
+ return n, nil
}
+ decodedLen := 0
// definition + repetition levels are always uncompressed
if levelsBytelen > 0 {
- if _, err := io.ReadFull(rd, buf[:levelsBytelen]); err != nil {
- return err
+ n, err := io.ReadFull(rd, buf[:levelsBytelen])
+ if err != nil {
+ return n, err
+ }
+ decodedLen += n
+ if n != levelsBytelen {
+ return n, fmt.Errorf("parquet: expected to read %d
bytes for levels, got %d", levelsBytelen, n)
}
}
- if _, err := p.decompress(p.r, lenCompressed-levelsBytelen,
buf[levelsBytelen:]); err != nil {
- return err
+ values, err := p.decompress(p.r, lenCompressed-levelsBytelen,
buf[levelsBytelen:])
+ if err != nil {
+ return decodedLen, err
}
- return nil
+ decodedLen += len(values)
+ if decodedLen != lenUncompressed {
+ return decodedLen, fmt.Errorf("parquet: metadata said %d bytes
uncompressed data page, got %d bytes", lenUncompressed, decodedLen)
+ }
+ return decodedLen, nil
}
type dataheader interface {
@@ -864,20 +896,25 @@ func (p *serializedPageReader) Next() bool {
}
if p.cryptoCtx.DataDecryptor != nil {
- if err := p.readV2Encrypted(p.r, lenCompressed,
levelsBytelen, compressed, buf.Bytes()); err != nil {
+ got, err := p.readV2Encrypted(p.r,
lenCompressed, lenUncompressed, levelsBytelen, compressed, buf.Bytes())
+ if err != nil {
p.err = err
return false
}
+ if got != lenUncompressed {
+ p.err = fmt.Errorf("parquet: metadata
said %d bytes uncompressed data page, got %d bytes", lenUncompressed, got)
+ return false
+ }
} else {
- if err := p.readV2Unencrypted(p.r,
lenCompressed, levelsBytelen, compressed, buf.Bytes()); err != nil {
+ got, err := p.readV2Unencrypted(p.r,
lenCompressed, lenUncompressed, levelsBytelen, compressed, buf.Bytes())
+ if err != nil {
p.err = err
return false
}
- }
-
- if buf.Len() != lenUncompressed {
- p.err = fmt.Errorf("parquet: metadata said %d
bytes uncompressed data page, got %d bytes", lenUncompressed, buf.Len())
- return false
+ if got != lenUncompressed {
+ p.err = fmt.Errorf("parquet: metadata
said %d bytes uncompressed data page, got %d bytes", lenUncompressed, got)
+ return false
+ }
}
// make datapage v2