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 27952c74 fix(parquet/variant): prevent metadata offset cursor overflow
(#1290)
27952c74 is described below
commit 27952c740323854874284c355251d3c17c29e648
Author: matanper <[email protected]>
AuthorDate: Tue Sep 8 20:10:02 2026 +0300
fix(parquet/variant): prevent metadata offset cursor overflow (#1290)
### Rationale for this change
Reading valid VARIANT metadata with 126 dictionary keys and two-byte
offsets panics with `slice bounds out of range [255:1]`. The metadata
offset cursor inherits `uint8` from `offsetSz`; both advancing the
cursor and computing the slice end can wrap at byte 256. Three- and
four-byte offset tables hit the same problem with even fewer keys.
This also affects current main after #1063: validating the complete
offset table does not prevent the cursor arithmetic from overflowing
while reading that valid table.
### What changes are included in this PR?
Use an `int` offset width and cursor for indexing the metadata buffer.
Keep the wire-format width and existing metadata validation unchanged.
Add synthetic valid-metadata tests immediately before, at, and after the
failure boundary for two-, three-, and four-byte offsets, plus larger
dictionaries. Check every decoded dictionary key. No external test data
is required.
### Are these changes tested?
- Before the fix, `go test ./parquet/variant -run
TestMetadataOffsetTableCrossesByteBoundary -count=1` reproduces the
panic at `width_2/keys_126`.
- After the fix, `go test ./parquet/variant ./arrow/extensions` passes,
including all 13 new boundary cases.
### Are there any user-facing changes?
Valid VARIANT dictionaries spanning byte 255 decode successfully instead
of panicking. There are no public API or format changes.
Fixes #1291.
---
parquet/variant/variant.go | 10 +++---
.../variant_metadata_offset_boundary_test.go | 41 ++++++++++++++++++++++
2 files changed, 47 insertions(+), 4 deletions(-)
diff --git a/parquet/variant/variant.go b/parquet/variant/variant.go
index a4557004..35f72c73 100644
--- a/parquet/variant/variant.go
+++ b/parquet/variant/variant.go
@@ -200,16 +200,18 @@ func (m *Metadata) loadDictionary(offsetSz uint8) error {
ErrInvalidMetadata, valuesStart, len(m.data))
}
- offsetPos := hdrSizeBytes + offsetSz
- if first := readLEU32(m.data[offsetPos : offsetPos+offsetSz]); first !=
0 {
+ // The table position can exceed 255 even when offsets use only two
bytes.
+ offsetWidth := int(offsetSz)
+ offsetPos := hdrSizeBytes + offsetWidth
+ if first := readLEU32(m.data[offsetPos : offsetPos+offsetWidth]); 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])
+ offsetPos += offsetWidth
+ end := readLEU32(m.data[offsetPos : offsetPos+offsetWidth])
if end < offsetStart {
return fmt.Errorf("%w: offsets are not monotonic: %d <
%d",
ErrInvalidMetadata, end, offsetStart)
diff --git a/parquet/variant/variant_metadata_offset_boundary_test.go
b/parquet/variant/variant_metadata_offset_boundary_test.go
index ae22fc1c..555b3f4e 100644
--- a/parquet/variant/variant_metadata_offset_boundary_test.go
+++ b/parquet/variant/variant_metadata_offset_boundary_test.go
@@ -17,6 +17,7 @@
package variant_test
import (
+ "fmt"
"testing"
"github.com/apache/arrow-go/v18/parquet/variant"
@@ -37,3 +38,43 @@ func TestMetadataOffsetTableMayEndAtInputBoundary(t
*testing.T) {
require.NoError(t, err)
require.Empty(t, key)
}
+
+// Multi-byte offset tables can cross byte 255 even for small dictionaries.
+func TestMetadataOffsetTableCrossesByteBoundary(t *testing.T) {
+ for _, tc := range []struct{ width, keys int }{
+ {2, 125}, {2, 126}, {2, 127}, {2, 128}, {2, 256},
+ {3, 82}, {3, 83}, {3, 84}, {3, 256},
+ {4, 61}, {4, 62}, {4, 63}, {4, 256},
+ } {
+ t.Run(fmt.Sprintf("width_%d/keys_%d", tc.width, tc.keys),
func(t *testing.T) {
+ // Use explicitly encoded, valid metadata to exercise
each offset width
+ // independently of the builder's choice of the
smallest representation.
+ encoded := []byte{byte(1 | (tc.width-1)<<6)}
+ appendOffset := func(value int) {
+ for i := 0; i < tc.width; i++ {
+ encoded = append(encoded,
byte(value>>(8*i)))
+ }
+ }
+ appendOffset(tc.keys)
+ keys := make([]string, tc.keys)
+ offset := 0
+ for i := range keys {
+ keys[i] = fmt.Sprintf("key_%04d", i)
+ appendOffset(offset)
+ offset += len(keys[i])
+ }
+ appendOffset(offset)
+ for _, key := range keys {
+ encoded = append(encoded, key...)
+ }
+ metadata, err := variant.NewMetadata(encoded)
+ require.NoError(t, err)
+ require.EqualValues(t, tc.keys,
metadata.DictionarySize())
+ for i, want := range keys {
+ got, err := metadata.KeyAt(uint32(i))
+ require.NoError(t, err)
+ require.Equal(t, want, got)
+ }
+ })
+ }
+}