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 970a12b5 fix(parquet/encoding): reject truncated plain boolean data 
(#1031)
970a12b5 is described below

commit 970a12b5ee93b67119b03bca22bcfc9635a27f0a
Author: Minh Vu <[email protected]>
AuthorDate: Tue Jul 28 17:07:02 2026 +0200

    fix(parquet/encoding): reject truncated plain boolean data (#1031)
    
    ### Rationale for this change
    
    The plain Boolean decoder trusted that each requested physical value had
    a corresponding payload bit. A truncated page could therefore panic
    during `Decode`, `Discard`, or `DecodeToBitmap` when the decoder indexed
    beyond the input buffer.
    
    The value count passed to `SetData` is only an upper bound because it
    includes null rows, so payload bounds must be checked when physical
    values are consumed.
    
    ### What changes are included in this PR?
    
    * Reject negative Boolean value counts in `SetData`.
    * Bound `Decode`, `Discard`, and `DecodeToBitmap` against the payload
    bits currently available.
    * Return an error wrapping `io.ErrUnexpectedEOF` without consuming
    decoder state.
    * Continue accepting nullable pages whose level count exceeds their
    physical Boolean count.
    
    ### Are these changes tested?
    
    Yes. The regression test covers all three truncated consumption paths
    and a nullable-style page with 100 levels and four physical values. The
    full `parquet/...` test tree passes, including the nullable V1/V2,
    spaced-bitmap, and Parquet-to-Arrow paths.
---
 parquet/internal/encoding/boolean_decoder.go | 20 ++++++++++++++++
 parquet/internal/encoding/encoding_test.go   | 36 ++++++++++++++++++++++++++++
 2 files changed, 56 insertions(+)

diff --git a/parquet/internal/encoding/boolean_decoder.go 
b/parquet/internal/encoding/boolean_decoder.go
index b644baf9..84924742 100644
--- a/parquet/internal/encoding/boolean_decoder.go
+++ b/parquet/internal/encoding/boolean_decoder.go
@@ -43,6 +43,9 @@ func (PlainBooleanDecoder) Type() parquet.Type {
 }
 
 func (dec *PlainBooleanDecoder) SetData(nvals int, data []byte) error {
+       if nvals < 0 {
+               return fmt.Errorf("parquet: invalid number of boolean values: 
%d", nvals)
+       }
        if err := dec.decoder.SetData(nvals, data); err != nil {
                return err
        }
@@ -50,8 +53,19 @@ func (dec *PlainBooleanDecoder) SetData(nvals int, data 
[]byte) error {
        return nil
 }
 
+func (dec *PlainBooleanDecoder) ensureBitsAvailable(n int) error {
+       available := int64(len(dec.data))*8 - int64(dec.bitOffset)
+       if int64(n) > available {
+               return fmt.Errorf("parquet: boolean data has %d bits available, 
need %d: %w", available, n, io.ErrUnexpectedEOF)
+       }
+       return nil
+}
+
 func (dec *PlainBooleanDecoder) Discard(n int) (int, error) {
        n = min(n, dec.nvals)
+       if err := dec.ensureBitsAvailable(n); err != nil {
+               return 0, err
+       }
        dec.nvals -= n
 
        if dec.bitOffset+n < 8 {
@@ -77,6 +91,9 @@ func (dec *PlainBooleanDecoder) Discard(n int) (int, error) {
 // Returns the number of values decoded
 func (dec *PlainBooleanDecoder) Decode(out []bool) (int, error) {
        max := shared_utils.Min(len(out), dec.nvals)
+       if err := dec.ensureBitsAvailable(max); err != nil {
+               return 0, err
+       }
 
        // attempts to read all remaining bool values from the current data byte
        unalignedExtract := func(i int) int {
@@ -127,6 +144,9 @@ func (dec *PlainBooleanDecoder) DecodeToBitmap(out []byte, 
outOffset int64, leng
        if max == 0 {
                return 0, nil
        }
+       if err := dec.ensureBitsAvailable(max); err != nil {
+               return 0, err
+       }
 
        // Check if we're aligned and can do a fast copy
        if dec.bitOffset == 0 && outOffset%8 == 0 {
diff --git a/parquet/internal/encoding/encoding_test.go 
b/parquet/internal/encoding/encoding_test.go
index 8242e56e..7a73df08 100644
--- a/parquet/internal/encoding/encoding_test.go
+++ b/parquet/internal/encoding/encoding_test.go
@@ -1089,6 +1089,42 @@ func TestBooleanPlainDecoderAfterFlushing(t *testing.T) {
        assert.Equal(t, decSlice[0], false)
 }
 
+func TestBooleanPlainDecoderRejectsTruncatedData(t *testing.T) {
+       descr := schema.NewColumn(schema.NewBooleanNode("bool", 
parquet.Repetitions.Required, -1), 0, 0)
+       type booleanBitmapDecoder interface {
+               encoding.BooleanDecoder
+               DecodeToBitmap([]byte, int64, int) (int, error)
+       }
+       newDecoder := func() booleanBitmapDecoder {
+               dec := encoding.NewDecoder(parquet.Types.Boolean, 
parquet.Encodings.Plain, descr, memory.DefaultAllocator).(booleanBitmapDecoder)
+               require.NoError(t, dec.SetData(9, []byte{0xff}))
+               return dec
+       }
+
+       dec := newDecoder()
+       n, err := dec.Decode(make([]bool, 9))
+       assert.Zero(t, n)
+       assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
+
+       dec = newDecoder()
+       n, err = dec.Discard(9)
+       assert.Zero(t, n)
+       assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
+
+       dec = newDecoder()
+       n, err = dec.DecodeToBitmap(make([]byte, 2), 0, 9)
+       assert.Zero(t, n)
+       assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
+
+       dec = encoding.NewDecoder(parquet.Types.Boolean, 
parquet.Encodings.Plain, descr, memory.DefaultAllocator).(booleanBitmapDecoder)
+       require.NoError(t, dec.SetData(100, []byte{0x0f}))
+       n, err = dec.Decode(make([]bool, 4))
+       assert.Equal(t, 4, n)
+       assert.NoError(t, err)
+
+       assert.Error(t, dec.SetData(-1, nil))
+}
+
 func TestBooleanPlainEncoderPutBitmap(t *testing.T) {
        descr := schema.NewColumn(schema.NewBooleanNode("bool", 
parquet.Repetitions.Optional, -1), 0, 0)
        enc := encoding.NewEncoder(parquet.Types.Boolean, 
parquet.Encodings.Plain, false, descr, memory.DefaultAllocator)

Reply via email to