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 818c813c fix(cdata): validate imported array sizes (#931)
818c813c is described below

commit 818c813c9c1f6692c5d2212fdb26549dd68212d2
Author: Minh Vu <[email protected]>
AuthorDate: Wed Jul 15 17:59:27 2026 +0200

    fix(cdata): validate imported array sizes (#931)
---
 arrow/cdata/cdata.go                | 211 ++++++++++++++++++++++++++++++++++--
 arrow/cdata/cdata_test.go           | 201 ++++++++++++++++++++++++++++++++++
 arrow/cdata/cdata_test_framework.go |  18 +++
 3 files changed, 423 insertions(+), 7 deletions(-)

diff --git a/arrow/cdata/cdata.go b/arrow/cdata/cdata.go
index 68c6f7e3..c4fcad34 100644
--- a/arrow/cdata/cdata.go
+++ b/arrow/cdata/cdata.go
@@ -349,13 +349,147 @@ type cimporter struct {
        alloc *importAllocator
 }
 
+const maxInt64Value = int64(1<<63 - 1)
+
 func (imp *cimporter) importChild(parent *cimporter, src *CArrowArray) error {
        imp.parent, imp.arr, imp.alloc = parent, src, parent.alloc
        return imp.doImport()
 }
 
+func validateCArrayHeader(arr *CArrowArray) error {
+       if arr == nil {
+               return fmt.Errorf("%w: nil ArrowArray", arrow.ErrInvalid)
+       }
+       length := int64(arr.length)
+       offset := int64(arr.offset)
+       nullCount := int64(arr.null_count)
+       nBuffers := int64(arr.n_buffers)
+       nChildren := int64(arr.n_children)
+
+       if length < 0 {
+               return fmt.Errorf("%w: ArrowArray length cannot be negative: 
%d", arrow.ErrInvalid, length)
+       }
+       if offset < 0 {
+               return fmt.Errorf("%w: ArrowArray offset cannot be negative: 
%d", arrow.ErrInvalid, offset)
+       }
+       if nullCount < -1 || nullCount > length {
+               return fmt.Errorf("%w: ArrowArray null_count %d is outside [-1, 
%d]", arrow.ErrInvalid, nullCount, length)
+       }
+       if nBuffers < 0 {
+               return fmt.Errorf("%w: ArrowArray n_buffers cannot be negative: 
%d", arrow.ErrInvalid, nBuffers)
+       }
+       if nChildren < 0 {
+               return fmt.Errorf("%w: ArrowArray n_children cannot be 
negative: %d", arrow.ErrInvalid, nChildren)
+       }
+       if nBuffers > maxIntValue() {
+               return fmt.Errorf("%w: ArrowArray n_buffers is too large: %d", 
arrow.ErrInvalid, nBuffers)
+       }
+       if nChildren > maxIntValue() {
+               return fmt.Errorf("%w: ArrowArray n_children is too large: %d", 
arrow.ErrInvalid, nChildren)
+       }
+       if _, err := checkedMul(nBuffers, int64(unsafe.Sizeof(uintptr(0)))); 
err != nil {
+               return fmt.Errorf("%w: ArrowArray buffers pointer array is too 
large", arrow.ErrInvalid)
+       }
+       if _, err := checkedMul(nChildren, int64(unsafe.Sizeof(uintptr(0)))); 
err != nil {
+               return fmt.Errorf("%w: ArrowArray children pointer array is too 
large", arrow.ErrInvalid)
+       }
+       if nBuffers > 0 && arr.buffers == nil {
+               return fmt.Errorf("%w: ArrowArray buffers is nil with n_buffers 
%d", arrow.ErrInvalid, nBuffers)
+       }
+       if nChildren > 0 && arr.children == nil {
+               return fmt.Errorf("%w: ArrowArray children is nil with 
n_children %d", arrow.ErrInvalid, nChildren)
+       }
+       if offset > maxInt64Value-length {
+               return fmt.Errorf("%w: ArrowArray length and offset overflow: 
length %d, offset %d", arrow.ErrInvalid, length, offset)
+       }
+       if length+offset > maxIntValue() {
+               return fmt.Errorf("%w: ArrowArray length and offset exceed 
native int: length %d, offset %d", arrow.ErrInvalid, length, offset)
+       }
+
+       return nil
+}
+
+func maxIntValue() int64 {
+       return int64(^uint(0) >> 1)
+}
+
+func checkedAdd(a, b int64) (int64, error) {
+       if a < 0 || b < 0 || a > maxInt64Value-b {
+               return 0, fmt.Errorf("%w: integer addition overflow: %d + %d", 
arrow.ErrInvalid, a, b)
+       }
+       return a + b, nil
+}
+
+func checkedMul(a, b int64) (int64, error) {
+       if a < 0 || b < 0 || (a != 0 && b > maxInt64Value/a) {
+               return 0, fmt.Errorf("%w: integer multiplication overflow: %d * 
%d", arrow.ErrInvalid, a, b)
+       }
+       return a * b, nil
+}
+
+func checkedBytesForBits(bits int64) (int64, error) {
+       bits, err := checkedAdd(bits, 7)
+       if err != nil {
+               return 0, err
+       }
+       return bits >> 3, nil
+}
+
+func (imp *cimporter) checkExpectedChildren() error {
+       var expected int64
+       switch imp.dt.ID() {
+       case arrow.LIST, arrow.LARGE_LIST, arrow.LIST_VIEW, 
arrow.LARGE_LIST_VIEW, arrow.FIXED_SIZE_LIST, arrow.MAP:
+               expected = 1
+       case arrow.STRUCT:
+               expected = int64(len(imp.dt.(*arrow.StructType).Fields()))
+       case arrow.RUN_END_ENCODED:
+               expected = 2
+       case arrow.DENSE_UNION:
+               expected = int64(len(imp.dt.(*arrow.DenseUnionType).Fields()))
+       case arrow.SPARSE_UNION:
+               expected = int64(len(imp.dt.(*arrow.SparseUnionType).Fields()))
+       }
+       return imp.checkNumChildren(expected)
+}
+
+func (imp *cimporter) checkExpectedBuffers() error {
+       switch imp.dt.(type) {
+       case *arrow.NullType, *arrow.RunEndEncodedType:
+               return imp.checkNumBuffers(0)
+       case arrow.FixedWidthDataType:
+               return imp.checkNumBuffers(2)
+       case *arrow.StringType, *arrow.BinaryType, *arrow.LargeStringType, 
*arrow.LargeBinaryType:
+               return imp.checkNumBuffers(3)
+       case *arrow.StringViewType, *arrow.BinaryViewType:
+               if imp.arr.n_buffers < 3 {
+                       return fmt.Errorf("%w: expected at least 3 buffers for 
imported type %s, ArrowArray has %d", arrow.ErrInvalid, imp.dt, 
imp.arr.n_buffers)
+               }
+       case *arrow.ListType, *arrow.LargeListType, *arrow.MapType:
+               return imp.checkNumBuffers(2)
+       case *arrow.ListViewType, *arrow.LargeListViewType:
+               return imp.checkNumBuffers(3)
+       case *arrow.FixedSizeListType, *arrow.StructType:
+               return imp.checkNumBuffers(1)
+       case *arrow.DenseUnionType:
+               if imp.arr.n_buffers != 2 && imp.arr.n_buffers != 3 {
+                       return fmt.Errorf("%w: expected 2 or 3 buffers for 
imported type %s, ArrowArray has %d", arrow.ErrInvalid, imp.dt, 
imp.arr.n_buffers)
+               }
+       case *arrow.SparseUnionType:
+               if imp.arr.n_buffers != 1 && imp.arr.n_buffers != 2 {
+                       return fmt.Errorf("%w: expected 1 or 2 buffers for 
imported type %s, ArrowArray has %d", arrow.ErrInvalid, imp.dt, 
imp.arr.n_buffers)
+               }
+       default:
+               return fmt.Errorf("unimplemented type %s", imp.dt)
+       }
+       return nil
+}
+
 // import any child arrays for lists, structs, and so on.
 func (imp *cimporter) doImportChildren() error {
+       if err := imp.checkExpectedChildren(); err != nil {
+               return err
+       }
+
        var children []*CArrowArray
        if imp.arr.n_children > 0 {
                children = unsafe.Slice(imp.arr.children, imp.arr.n_children)
@@ -441,6 +575,9 @@ func (imp *cimporter) initarr() {
 }
 
 func (imp *cimporter) doImportArr(src *CArrowArray) error {
+       if err := validateCArrayHeader(src); err != nil {
+               return err
+       }
        imp.arr = C.get_arr()
        C.ArrowArrayMove(src, imp.arr)
        if imp.alloc == nil {
@@ -468,6 +605,16 @@ func (imp *cimporter) doImportArr(src *CArrowArray) error {
 // import is called recursively as needed for importing an array and its 
children
 // in order to generate array.Data objects
 func (imp *cimporter) doImport() error {
+       if err := validateCArrayHeader(imp.arr); err != nil {
+               return err
+       }
+       if _, ok := imp.dt.(*arrow.DictionaryType); ok && imp.arr.dictionary == 
nil {
+               return fmt.Errorf("%w: dictionary ArrowArray has no 
dictionary", arrow.ErrInvalid)
+       }
+       if err := imp.checkExpectedBuffers(); err != nil {
+               return err
+       }
+
        // import any children
        if err := imp.doImportChildren(); err != nil {
                for _, c := range imp.children {
@@ -686,6 +833,9 @@ func (imp *cimporter) importBinaryViewLike() (err error) {
        if err = imp.checkNoChildren(); err != nil {
                return
        }
+       if _, err = checkedMul(int64(imp.arr.n_buffers)-3, 
int64(arrow.Int64SizeBytes)); err != nil {
+               return err
+       }
 
        buffers := make([]*memory.Buffer, len(imp.cbuffers)-1)
        defer memory.ReleaseBuffers(buffers)
@@ -699,6 +849,12 @@ func (imp *cimporter) importBinaryViewLike() (err error) {
        }
 
        if len(buffers) > 2 {
+               if imp.cbuffers[len(buffers)] == nil {
+                       return fmt.Errorf("%w: invalid data buffer sizes", 
arrow.ErrInvalid)
+               }
+               if _, err = checkedMul(int64(len(buffers)-2), 
int64(arrow.Int64SizeBytes)); err != nil {
+                       return err
+               }
                dataBufferSizes := 
unsafe.Slice((*int64)(unsafe.Pointer(imp.cbuffers[len(buffers)])), 
len(buffers)-2)
                for i, size := range dataBufferSizes {
                        if buffers[i+2], err = 
imp.importVariableValuesBuffer(i+2, 1, size); err != nil {
@@ -795,7 +951,12 @@ func (imp *cimporter) importFixedSizePrimitive() error {
 
        fw := imp.dt.(arrow.FixedWidthDataType)
        if bitutil.IsMultipleOf8(int64(fw.BitWidth())) {
-               values, err = imp.importFixedSizeBuffer(1, 
bitutil.BytesForBits(int64(fw.BitWidth())))
+               var byteWidth int64
+               byteWidth, err = checkedBytesForBits(int64(fw.BitWidth()))
+               if err != nil {
+                       return err
+               }
+               values, err = imp.importFixedSizeBuffer(1, byteWidth)
        } else {
                if fw.BitWidth() != 1 {
                        return errors.New("invalid bitwidth")
@@ -853,11 +1014,19 @@ func (imp *cimporter) checkNumBuffers(n int64) error {
 }
 
 func (imp *cimporter) importBuffer(bufferID int, sz int64) (*memory.Buffer, 
error) {
+       // Buffer sizes come from foreign memory and cannot be independently 
verified;
+       // these checks only prevent invalid metadata from creating unsafe Go 
slices.
        // this is not a copy, we're just having a slice which points at the 
data
        // it's still owned by the C.ArrowArray object and its backing C++ 
object.
+       if bufferID < 0 || bufferID >= len(imp.cbuffers) {
+               return nil, fmt.Errorf("%w: invalid buffer index %d", 
arrow.ErrInvalid, bufferID)
+       }
+       if sz < 0 || sz > maxIntValue() {
+               return nil, fmt.Errorf("%w: invalid buffer size %d", 
arrow.ErrInvalid, sz)
+       }
        if imp.cbuffers[bufferID] == nil || sz == 0 {
                if sz != 0 {
-                       return nil, errors.New("invalid buffer")
+                       return nil, fmt.Errorf("%w: invalid buffer", 
arrow.ErrInvalid)
                }
                return memory.NewBufferBytes([]byte{}), nil
        }
@@ -867,7 +1036,14 @@ func (imp *cimporter) importBuffer(bufferID int, sz 
int64) (*memory.Buffer, erro
 }
 
 func (imp *cimporter) importBitsBuffer(bufferID int) (*memory.Buffer, error) {
-       bufsize := bitutil.BytesForBits(int64(imp.arr.length) + 
int64(imp.arr.offset))
+       total, err := checkedAdd(int64(imp.arr.length), int64(imp.arr.offset))
+       if err != nil {
+               return nil, err
+       }
+       bufsize, err := checkedBytesForBits(total)
+       if err != nil {
+               return nil, err
+       }
        return imp.importBuffer(bufferID, bufsize)
 }
 
@@ -884,18 +1060,39 @@ func (imp *cimporter) importNullBitmap(bufferID int) 
(*memory.Buffer, error) {
 }
 
 func (imp *cimporter) importFixedSizeBuffer(bufferID int, byteWidth int64) 
(*memory.Buffer, error) {
-       bufsize := byteWidth * int64(imp.arr.length+imp.arr.offset)
+       total, err := checkedAdd(int64(imp.arr.length), int64(imp.arr.offset))
+       if err != nil {
+               return nil, err
+       }
+       bufsize, err := checkedMul(byteWidth, total)
+       if err != nil {
+               return nil, err
+       }
        return imp.importBuffer(bufferID, bufsize)
 }
 
 func (imp *cimporter) importOffsetsBuffer(bufferID int, offsetsize int64) 
(*memory.Buffer, error) {
-       bufsize := offsetsize * int64((imp.arr.length + imp.arr.offset + 1))
+       total, err := checkedAdd(int64(imp.arr.length), int64(imp.arr.offset))
+       if err != nil {
+               return nil, err
+       }
+       total, err = checkedAdd(total, 1)
+       if err != nil {
+               return nil, err
+       }
+       bufsize, err := checkedMul(offsetsize, total)
+       if err != nil {
+               return nil, err
+       }
        return imp.importBuffer(bufferID, bufsize)
 }
 
 func (imp *cimporter) importVariableValuesBuffer(bufferID int, byteWidth, 
nvals int64) (*memory.Buffer, error) {
-       bufsize := byteWidth * nvals
-       return imp.importBuffer(bufferID, int64(bufsize))
+       bufsize, err := checkedMul(byteWidth, nvals)
+       if err != nil {
+               return nil, err
+       }
+       return imp.importBuffer(bufferID, bufsize)
 }
 
 func importCArrayAsType(arr *CArrowArray, dt arrow.DataType) (imp *cimporter, 
err error) {
diff --git a/arrow/cdata/cdata_test.go b/arrow/cdata/cdata_test.go
index c196b215..776f17a3 100644
--- a/arrow/cdata/cdata_test.go
+++ b/arrow/cdata/cdata_test.go
@@ -1102,6 +1102,207 @@ func TestEmptyUnionExport(t *testing.T) {
        assert.NotEqualValues(t, unsafe.Pointer(nil), buffers[0])
 }
 
+func TestValidateCArrayHeader(t *testing.T) {
+       const maxInt64 = int64(1<<63 - 1)
+
+       tests := []struct {
+               name  string
+               setup func(*CArrowArray)
+       }{
+               {
+                       name: "nil array",
+                       setup: func(arr *CArrowArray) {
+                               _ = arr
+                       },
+               },
+               {
+                       name:  "negative length",
+                       setup: func(arr *CArrowArray) { arr.length = -1 },
+               },
+               {
+                       name:  "negative offset",
+                       setup: func(arr *CArrowArray) { arr.offset = -1 },
+               },
+               {
+                       name:  "null count below unknown",
+                       setup: func(arr *CArrowArray) { arr.null_count = -2 },
+               },
+               {
+                       name:  "null count exceeds length",
+                       setup: func(arr *CArrowArray) { arr.null_count = 2 },
+               },
+               {
+                       name:  "negative buffer count",
+                       setup: func(arr *CArrowArray) { arr.n_buffers = -1 },
+               },
+               {
+                       name:  "negative child count",
+                       setup: func(arr *CArrowArray) { arr.n_children = -1 },
+               },
+               {
+                       name:  "huge buffer count",
+                       setup: func(arr *CArrowArray) { setCArrayCounts(arr, 
maxInt64, 0) },
+               },
+               {
+                       name:  "huge child count",
+                       setup: func(arr *CArrowArray) { setCArrayCounts(arr, 0, 
maxInt64) },
+               },
+               {
+                       name:  "missing buffers",
+                       setup: func(arr *CArrowArray) { arr.n_buffers = 1 },
+               },
+               {
+                       name:  "missing children",
+                       setup: func(arr *CArrowArray) { arr.n_children = 1 },
+               },
+               {
+                       name: "length and offset overflow",
+                       setup: func(arr *CArrowArray) {
+                               setCArrayLength(arr, maxInt64)
+                               arr.offset = 1
+                       },
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       if tt.name == "nil array" {
+                               require.Error(t, validateCArrayHeader(nil))
+                               return
+                       }
+
+                       arr := CArrowArray{}
+                       arr.length = 1
+                       tt.setup(&arr)
+                       require.Error(t, validateCArrayHeader(&arr))
+               })
+       }
+}
+
+func TestCArrayImportValidatesHeaderBeforeNativeSlices(t *testing.T) {
+       const maxInt64 = int64(1<<63 - 1)
+
+       tests := []struct {
+               name   string
+               arr    CArrowArray
+               length int64
+               dt     arrow.DataType
+       }{
+               {
+                       name: "missing child pointer",
+                       arr: CArrowArray{
+                               length:     1,
+                               n_children: 1,
+                       },
+                       dt: arrow.ListOf(arrow.PrimitiveTypes.Int32),
+               },
+               {
+                       name: "fixed width multiplication overflow",
+                       arr: CArrowArray{
+                               n_buffers: 2,
+                       },
+                       length: maxInt64/4 + 1,
+                       dt:     arrow.PrimitiveTypes.Int32,
+               },
+               {
+                       name: "offset multiplication overflow",
+                       arr: CArrowArray{
+                               n_buffers: 3,
+                       },
+                       length: maxInt64/4 + 1,
+                       dt:     arrow.BinaryTypes.String,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       arr := tt.arr
+                       if tt.length != 0 {
+                               setCArrayLength(&arr, tt.length)
+                       }
+                       if arr.n_buffers > 0 {
+                               setCArrayBuffers(&arr, int(arr.n_buffers))
+                               defer freeCArrayBuffers(&arr)
+                       }
+
+                       imp := &cimporter{arr: &arr, dt: tt.dt}
+                       require.NotPanics(t, func() {
+                               require.Error(t, imp.doImport())
+                       })
+               })
+       }
+}
+
+func TestImportCArrayRejectsInvalidRootHeaderBeforeMove(t *testing.T) {
+       arr := CArrowArray{}
+       arr.length = -1
+
+       require.NotPanics(t, func() {
+               _, err := importCArrayAsType(&arr, arrow.PrimitiveTypes.Int32)
+               require.Error(t, err)
+       })
+}
+
+func TestImportDictionaryArrayRequiresDictionary(t *testing.T) {
+       arr := CArrowArray{n_buffers: 2}
+       setCArrayBuffers(&arr, 2)
+       defer freeCArrayBuffers(&arr)
+
+       dt := &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int8,
+               ValueType: arrow.PrimitiveTypes.Int32,
+       }
+       imp := &cimporter{arr: &arr, dt: dt}
+
+       require.ErrorIs(t, imp.doImport(), arrow.ErrInvalid)
+}
+
+func TestImportBinaryViewRequiresSizeMetadata(t *testing.T) {
+       arr := CArrowArray{n_buffers: 4}
+       setCArrayBuffers(&arr, 4)
+       defer freeCArrayBuffers(&arr)
+
+       imp := &cimporter{arr: &arr, dt: &arrow.BinaryViewType{}}
+
+       require.ErrorIs(t, imp.doImport(), arrow.ErrInvalid)
+}
+
+func TestImportNestedArraysRejectsUnexpectedCounts(t *testing.T) {
+       tests := []struct {
+               name      string
+               dt        arrow.DataType
+               nBuffers  int64
+               nChildren int64
+       }{
+               {
+                       name:      "struct child count",
+                       dt:        arrow.StructOf(arrow.Field{Name: "value", 
Type: arrow.PrimitiveTypes.Int32}),
+                       nBuffers:  1,
+                       nChildren: 0,
+               },
+               {
+                       name:      "sparse union child count",
+                       dt:        arrow.SparseUnionOf([]arrow.Field{{Name: 
"value", Type: arrow.PrimitiveTypes.Int32}}, []arrow.UnionTypeCode{0}),
+                       nBuffers:  1,
+                       nChildren: 0,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       arr := CArrowArray{}
+                       setCArrayCounts(&arr, tt.nBuffers, tt.nChildren)
+                       if tt.nBuffers > 0 {
+                               setCArrayBuffers(&arr, int(tt.nBuffers))
+                               defer freeCArrayBuffers(&arr)
+                       }
+
+                       imp := &cimporter{arr: &arr, dt: tt.dt}
+                       require.Error(t, imp.doImport())
+               })
+       }
+}
+
 func TestRecordReaderExport(t *testing.T) {
        // Regression test for apache/arrow#33767
        reclist := arrdata.Records["primitives"]
diff --git a/arrow/cdata/cdata_test_framework.go 
b/arrow/cdata/cdata_test_framework.go
index 3e2c5bc8..a4b13b7d 100644
--- a/arrow/cdata/cdata_test_framework.go
+++ b/arrow/cdata/cdata_test_framework.go
@@ -127,6 +127,24 @@ func freeCArray(arr *CArrowArray) {
        C.free(unsafe.Pointer(arr))
 }
 
+func setCArrayLength(arr *CArrowArray, length int64) {
+       arr.length = C.int64_t(length)
+}
+
+func setCArrayCounts(arr *CArrowArray, nBuffers, nChildren int64) {
+       arr.n_buffers = C.int64_t(nBuffers)
+       arr.n_children = C.int64_t(nChildren)
+}
+
+func setCArrayBuffers(arr *CArrowArray, n int) {
+       arr.buffers = (*unsafe.Pointer)(C.calloc(C.size_t(n), 
C.size_t(unsafe.Sizeof(uintptr(0)))))
+}
+
+func freeCArrayBuffers(arr *CArrowArray) {
+       C.free(unsafe.Pointer(arr.buffers))
+       arr.buffers = nil
+}
+
 func isReleased(arr *CArrowArray) bool {
        return C.ArrowArrayIsReleased(arr) == 1
 }

Reply via email to