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 df81ee9d fix(parquet/internal/encoding): do not leak delta bit width 
buffers from decoders (#1394)
df81ee9d is described below

commit df81ee9d30b4fa7c7f69a987ffb712df56da66a4
Author: Matt Topol <[email protected]>
AuthorDate: Wed Oct 21 22:11:10 2026 +0100

    fix(parquet/internal/encoding): do not leak delta bit width buffers from 
decoders (#1394)
    
    ### Rationale for this change
    
    `deltaBitPackDecoder` allocated its per-miniblock bit-width buffer
    (`deltaBitWidths`) from the reader's allocator, but decoders have no
    `Release`, so the buffer was never returned. This leaks for:
    
    - DELTA_BINARY_PACKED int32/int64 columns, and
    - the temporary length decoders that DELTA_LENGTH_BYTE_ARRAY (one) and
    DELTA_BYTE_ARRAY (two) create inside `SetData` on every page.
    
    With a checked allocator, reading a 545-row binary column back through
    pqarrow left 64 bytes (DELTA_LENGTH_BYTE_ARRAY) and 128 bytes
    (DELTA_BYTE_ARRAY) allocated after everything was released, more for
    multi-page columns. Found while reviewing #1392.
    
    ### What changes are included in this PR?
    
    `deltaBitWidths` is now a reused `[]byte` on the Go heap, grown only
    when needed (the same pattern as the decoders' existing
    `lengthScratch`). The size bound is unchanged: the header validation
    already caps the miniblock count. No API change.
    
    ### Are these changes tested?
    
    Yes. `TestDeltaDecodersDoNotRetainAllocations` (in
    `delta_bit_packing_validation_test.go`) decodes DELTA_BINARY_PACKED,
    DELTA_LENGTH_BYTE_ARRAY and DELTA_BYTE_ARRAY data with a checked
    allocator and asserts nothing is left allocated. All three subtests fail
    on main (`LEAK of 64 bytes`) and pass with the change.
    
    - `go test ./parquet/internal/encoding ./parquet/file
    ./parquet/pqarrow`: ok
    - `go test -tags noasm ./parquet/internal/encoding` and `GOARCH=386 go
    test ./parquet/internal/encoding`: ok
    - `go vet ./parquet/internal/encoding` and `gofmt -l`: clean
    
    ### Are there any user-facing changes?
    
    No API changes. Readers using a checked or C-backed allocator no longer
    leak a small buffer per delta-encoded page.
---
 parquet/internal/encoding/delta_bit_packing.go     | 18 +++---
 .../encoding/delta_bit_packing_validation_test.go  | 64 ++++++++++++++++++++++
 2 files changed, 74 insertions(+), 8 deletions(-)

diff --git a/parquet/internal/encoding/delta_bit_packing.go 
b/parquet/internal/encoding/delta_bit_packing.go
index 3add6947..dba6f6f3 100644
--- a/parquet/internal/encoding/delta_bit_packing.go
+++ b/parquet/internal/encoding/delta_bit_packing.go
@@ -50,7 +50,9 @@ type deltaBitPackDecoder[T int32 | int64] struct {
        minDelta             int64
        miniBlockIdx         uint64
 
-       deltaBitWidths *memory.Buffer
+       // deltaBitWidths holds one byte per miniblock. It lives on the Go heap
+       // because decoders have no Release to return allocator memory.
+       deltaBitWidths []byte
        deltaBitWidth  byte
 
        totalValues uint64
@@ -78,9 +80,6 @@ func (d *deltaBitPackDecoder[T]) SetData(nvalues int, data 
[]byte) error {
        d.bitdecoder = utils.NewBitReader(bytes.NewReader(d.data))
        d.currentBlockVals = 0
        d.currentMiniBlockVals = 0
-       if d.deltaBitWidths == nil {
-               d.deltaBitWidths = memory.NewResizableBuffer(d.mem)
-       }
 
        var ok bool
        d.blockSize, ok = d.bitdecoder.GetVlqInt()
@@ -144,17 +143,20 @@ func (d *deltaBitPackDecoder[T]) initBlock() error {
        }
 
        // ensure we have enough space for our miniblocks to decode the widths
-       d.deltaBitWidths.Resize(int(d.miniBlocksPerBlock))
+       if uint64(cap(d.deltaBitWidths)) < d.miniBlocksPerBlock {
+               d.deltaBitWidths = make([]byte, d.miniBlocksPerBlock)
+       }
+       d.deltaBitWidths = d.deltaBitWidths[:d.miniBlocksPerBlock]
 
        var err error
        for i := uint64(0); i < d.miniBlocksPerBlock; i++ {
-               if d.deltaBitWidths.Bytes()[i], err = d.bitdecoder.ReadByte(); 
err != nil {
+               if d.deltaBitWidths[i], err = d.bitdecoder.ReadByte(); err != 
nil {
                        return err
                }
        }
 
        d.miniBlockIdx = 0
-       d.deltaBitWidth = d.deltaBitWidths.Bytes()[0]
+       d.deltaBitWidth = d.deltaBitWidths[0]
        d.currentBlockVals = uint32(d.blockSize)
        return nil
 }
@@ -165,7 +167,7 @@ func (d *deltaBitPackDecoder[T]) unpackNextMini() error {
        } else {
                d.miniBlockValues = d.miniBlockValues[:0]
        }
-       d.deltaBitWidth = d.deltaBitWidths.Bytes()[int(d.miniBlockIdx)]
+       d.deltaBitWidth = d.deltaBitWidths[int(d.miniBlockIdx)]
        maxBitWidth := byte(64)
        if d.Type() == parquet.Types.Int32 {
                maxBitWidth = 32
diff --git a/parquet/internal/encoding/delta_bit_packing_validation_test.go 
b/parquet/internal/encoding/delta_bit_packing_validation_test.go
index 0f71f804..8122dbc1 100644
--- a/parquet/internal/encoding/delta_bit_packing_validation_test.go
+++ b/parquet/internal/encoding/delta_bit_packing_validation_test.go
@@ -17,6 +17,8 @@
 package encoding
 
 import (
+       "bytes"
+       "fmt"
        "io"
        "testing"
 
@@ -126,3 +128,65 @@ func TestDeltaBitPackEncoderReleasesSpacedScratch(t 
*testing.T) {
        enc.PutSpaced([]int32{1, 2, 3, 4}, []byte{0x0f}, 0)
        enc.Release()
 }
+
+// The delta decoders, including the length decoders that 
DELTA_LENGTH_BYTE_ARRAY
+// and DELTA_BYTE_ARRAY create inside SetData, have no Release, so decoding 
must
+// not leave anything allocated from the reader's allocator.
+func TestDeltaDecodersDoNotRetainAllocations(t *testing.T) {
+       const n = 545
+       ints := make([]int32, n)
+       strs := make([]parquet.ByteArray, n)
+       for i := range n {
+               ints[i] = int32(i * 7)
+               strs[i] = parquet.ByteArray(fmt.Sprintf("prefix/%06d", i))
+       }
+
+       encode := func(enc TypedEncoder) []byte {
+               buf, err := enc.FlushValues()
+               require.NoError(t, err)
+               defer buf.Release()
+               enc.Release()
+               return bytes.Clone(buf.Bytes())
+       }
+
+       for _, tc := range []struct {
+               name   string
+               typ    parquet.Type
+               enc    parquet.Encoding
+               data   func() []byte
+               decode func(TypedDecoder) (int, error)
+       }{
+               {"delta_binary_packed", parquet.Types.Int32, 
parquet.Encodings.DeltaBinaryPacked,
+                       func() []byte {
+                               enc := NewEncoder(parquet.Types.Int32, 
parquet.Encodings.DeltaBinaryPacked, false, nil, 
memory.DefaultAllocator).(Int32Encoder)
+                               enc.Put(ints)
+                               return encode(enc)
+                       },
+                       func(d TypedDecoder) (int, error) { return 
d.(Int32Decoder).Decode(make([]int32, n)) }},
+               {"delta_length_byte_array", parquet.Types.ByteArray, 
parquet.Encodings.DeltaLengthByteArray,
+                       func() []byte {
+                               enc := NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.DeltaLengthByteArray, false, nil, 
memory.DefaultAllocator).(ByteArrayEncoder)
+                               enc.Put(strs)
+                               return encode(enc)
+                       },
+                       func(d TypedDecoder) (int, error) { return 
d.(ByteArrayDecoder).Decode(make([]parquet.ByteArray, n)) }},
+               {"delta_byte_array", parquet.Types.ByteArray, 
parquet.Encodings.DeltaByteArray,
+                       func() []byte {
+                               enc := NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.DeltaByteArray, false, nil, 
memory.DefaultAllocator).(ByteArrayEncoder)
+                               enc.Put(strs)
+                               return encode(enc)
+                       },
+                       func(d TypedDecoder) (int, error) { return 
d.(ByteArrayDecoder).Decode(make([]parquet.ByteArray, n)) }},
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       data := tc.data()
+                       mem := 
memory.NewCheckedAllocator(memory.DefaultAllocator)
+                       dec := NewDecoder(tc.typ, tc.enc, nil, mem)
+                       require.NoError(t, dec.SetData(n, data))
+                       got, err := tc.decode(dec)
+                       require.NoError(t, err)
+                       require.Equal(t, n, got)
+                       mem.AssertSize(t, 0)
+               })
+       }
+}

Reply via email to