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 2b7e5b09 fix(parquet/variant): validate metadata offset tables (#1063)
2b7e5b09 is described below

commit 2b7e5b09ca46c872f0c7fa10d605ee0ebc72bc7c
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 5 19:41:38 2026 +0200

    fix(parquet/variant): validate metadata offset tables (#1063)
    
    ## What changed
    
    Validate the complete Variant metadata offset table before allocating
    the key slice or reading offsets. The parser now also rejects a nonzero
    initial offset, decreasing offsets, and string offsets beyond the input.
    
    ## Why
    
    The previous size check omitted the final offset entry. Truncated input
    could therefore reach an out-of-bounds slice, while decreasing offsets
    could underflow during key-size calculation. A large untrusted
    dictionary count was also used to allocate before checking whether the
    offset table could fit in the input.
    
    Malformed metadata now consistently returns an error wrapping
    `ErrInvalidMetadata`.
    
    ## Testing
    
    - `go test ./parquet/variant`
---
 parquet/variant/variant.go                         | 36 +++++++++++---------
 .../variant_metadata_offset_boundary_test.go       | 39 ++++++++++++++++++++++
 parquet/variant/variant_test.go                    | 20 +++++++++++
 3 files changed, 80 insertions(+), 15 deletions(-)

diff --git a/parquet/variant/variant.go b/parquet/variant/variant.go
index 8e31969c..2eec9602 100644
--- a/parquet/variant/variant.go
+++ b/parquet/variant/variant.go
@@ -183,32 +183,38 @@ func (m *Metadata) loadDictionary(offsetSz uint8) error {
        }
 
        dictSize := readLEU32(m.data[hdrSizeBytes : hdrSizeBytes+offsetSz])
-       m.keys = make([][]byte, dictSize)
-
        if dictSize == 0 {
+               m.keys = nil
                return nil
        }
 
-       // first offset is always 0
-       offsetStart, offsetPos := uint32(0), hdrSizeBytes+offsetSz
-       valuesStart := hdrSizeBytes + (dictSize+2)*uint32(offsetSz)
-       if hdrSizeBytes+int(dictSize+1)*int(offsetSz) > len(m.data) {
-               return fmt.Errorf("%w: offset out of range: %d > %d",
-                       ErrInvalidMetadata, 
(dictSize+hdrSizeBytes)*uint32(offsetSz), len(m.data))
+       valuesStart := uint64(hdrSizeBytes) + 
(uint64(dictSize)+2)*uint64(offsetSz)
+       if valuesStart > uint64(len(m.data)) {
+               return fmt.Errorf("%w: offset table out of range: %d > %d",
+                       ErrInvalidMetadata, valuesStart, len(m.data))
+       }
+
+       offsetPos := hdrSizeBytes + offsetSz
+       if first := readLEU32(m.data[offsetPos : offsetPos+offsetSz]); first != 
0 {
+               return fmt.Errorf("%w: first offset must be zero: %d", 
ErrInvalidMetadata, first)
        }
 
+       m.keys = make([][]byte, dictSize)
+       offsetStart := uint32(0)
        for i := range dictSize {
                offsetPos += offsetSz
                end := readLEU32(m.data[offsetPos : offsetPos+offsetSz])
-
-               keySize := end - offsetStart
-               valStart := valuesStart + offsetStart
-               if valStart+keySize > uint32(len(m.data)) {
+               if end < offsetStart {
+                       return fmt.Errorf("%w: offsets are not monotonic: %d < 
%d",
+                               ErrInvalidMetadata, end, offsetStart)
+               }
+               if valuesStart+uint64(end) > uint64(len(m.data)) {
                        return fmt.Errorf("%w: string data out of range: %d + 
%d > %d",
-                               ErrInvalidMetadata, valStart, keySize, 
len(m.data))
+                               ErrInvalidMetadata, valuesStart, end, 
len(m.data))
                }
-               m.keys[i] = m.data[valStart : valStart+keySize]
-               offsetStart += keySize
+
+               m.keys[i] = m.data[valuesStart+uint64(offsetStart) : 
valuesStart+uint64(end)]
+               offsetStart = end
        }
 
        return nil
diff --git a/parquet/variant/variant_metadata_offset_boundary_test.go 
b/parquet/variant/variant_metadata_offset_boundary_test.go
new file mode 100644
index 00000000..ae22fc1c
--- /dev/null
+++ b/parquet/variant/variant_metadata_offset_boundary_test.go
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package variant_test
+
+import (
+       "testing"
+
+       "github.com/apache/arrow-go/v18/parquet/variant"
+       "github.com/stretchr/testify/require"
+)
+
+func TestMetadataOffsetTableMayEndAtInputBoundary(t *testing.T) {
+       metadata, err := variant.NewMetadata([]byte{
+               0x01, // version 1 with one-byte offsets
+               0x01, // one dictionary key
+               0x00, // first offset
+               0x00, // final offset: the key is empty
+       })
+       require.NoError(t, err)
+       require.EqualValues(t, 1, metadata.DictionarySize())
+
+       key, err := metadata.KeyAt(0)
+       require.NoError(t, err)
+       require.Empty(t, key)
+}
diff --git a/parquet/variant/variant_test.go b/parquet/variant/variant_test.go
index 37bc04c8..e8216013 100644
--- a/parquet/variant/variant_test.go
+++ b/parquet/variant/variant_test.go
@@ -574,6 +574,26 @@ func TestInvalidMetadata(t *testing.T) {
                        metadata: []byte{0x01, 0xFF, 0x00}, // Claims to have 
many keys but doesn't
                        errMsg:   "out of range",
                },
+               {
+                       name:     "truncated offset table",
+                       metadata: []byte{0x01, 0x01, 0x00},
+                       errMsg:   "offset table out of range",
+               },
+               {
+                       name:     "dictionary size overflows offset table",
+                       metadata: []byte{0xC1, 0xFF, 0xFF, 0xFF, 0xFF},
+                       errMsg:   "offset table out of range",
+               },
+               {
+                       name:     "nonzero first offset",
+                       metadata: []byte{0x01, 0x01, 0x01, 0x01},
+                       errMsg:   "first offset must be zero",
+               },
+               {
+                       name:     "decreasing offsets",
+                       metadata: []byte{0x01, 0x02, 0x00, 0x02, 0x01, 'a', 
'b'},
+                       errMsg:   "offsets are not monotonic",
+               },
                {
                        name:     "string data out of range",
                        metadata: []byte{0x01, 0x01, 0x00, 0x05},

Reply via email to