Skip to content

Commit a88c7cb

Browse files
fix(arrow/cdata): validate imported schema topology (#1046)
## Summary - validate C Data schema format pointers and child headers before indexing them - bound child pointer slices before constructing them from foreign counts - enforce child topology for list, map, union, and fixed-size list formats - reject non-integer dictionary indexes and non-struct record batch schemas - return `arrow.ErrInvalid` for malformed foreign schemas instead of panicking ## Testing - `go test -tags test ./arrow/cdata` The malformed-format and oversized-child-count tests also verify that the imported C schema is released on error.
1 parent 22da919 commit a88c7cb

4 files changed

Lines changed: 147 additions & 5 deletions

File tree

arrow/cdata/cdata.go

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,26 @@ func decodeCMetadata(md *C.char) arrow.Metadata {
157157

158158
// convert a C.ArrowSchema to an arrow.Field to maintain metadata with the schema
159159
func importSchema(schema *CArrowSchema) (ret arrow.Field, err error) {
160+
if schema == nil {
161+
return ret, fmt.Errorf("%w: nil ArrowSchema", arrow.ErrInvalid)
162+
}
160163
// always release, even on error
161164
defer C.ArrowSchemaRelease(schema)
165+
if schema.format == nil {
166+
return ret, fmt.Errorf("%w: ArrowSchema format is nil", arrow.ErrInvalid)
167+
}
168+
if schema.n_children < 0 {
169+
return ret, fmt.Errorf("%w: ArrowSchema n_children cannot be negative: %d", arrow.ErrInvalid, schema.n_children)
170+
}
171+
if int64(schema.n_children) > maxIntValue() {
172+
return ret, fmt.Errorf("%w: ArrowSchema n_children is too large: %d", arrow.ErrInvalid, schema.n_children)
173+
}
174+
if _, err := checkedMul(int64(schema.n_children), int64(unsafe.Sizeof(uintptr(0)))); err != nil {
175+
return ret, fmt.Errorf("%w: ArrowSchema children pointer array is too large", arrow.ErrInvalid)
176+
}
177+
if schema.n_children > 0 && schema.children == nil {
178+
return ret, fmt.Errorf("%w: ArrowSchema children is nil with n_children %d", arrow.ErrInvalid, schema.n_children)
179+
}
162180

163181
var childFields []arrow.Field
164182
if schema.n_children > 0 {
@@ -181,12 +199,18 @@ func importSchema(schema *CArrowSchema) (ret arrow.Field, err error) {
181199

182200
// copies the c-string here, but it's very small
183201
f := C.GoString(schema.format)
202+
if f == "" {
203+
return ret, fmt.Errorf("%w: ArrowSchema format is empty", arrow.ErrInvalid)
204+
}
184205
// handle our non-parameterized simple types.
185206
dt, ok := formatToSimpleType[f]
186207
if ok {
187208
ret.Type = dt
188209

189210
if schema.dictionary != nil {
211+
if !arrow.IsInteger(ret.Type.ID()) {
212+
return ret, fmt.Errorf("%w: dictionary index type must be an integer", arrow.ErrInvalid)
213+
}
190214
valueField, err := importSchema(schema.dictionary)
191215
if err != nil {
192216
return ret, err
@@ -215,7 +239,10 @@ func importSchema(schema *CArrowSchema) (ret arrow.Field, err error) {
215239
case "w": // fixed size binary is "w:##" where ## is the byteWidth
216240
byteWidth, err := strconv.Atoi(val)
217241
if err != nil {
218-
return ret, err
242+
return ret, fmt.Errorf("%w: invalid fixed-size binary format %q: %v", arrow.ErrInvalid, f, err)
243+
}
244+
if byteWidth <= 0 {
245+
return ret, fmt.Errorf("%w: fixed-size binary byte width must be positive: %d", arrow.ErrInvalid, byteWidth)
219246
}
220247
dt = &arrow.FixedSizeBinaryType{ByteWidth: byteWidth}
221248
case "d": // decimal types are d:<precision>,<scale>[,<bitsize>] size is assumed 128 if left out
@@ -258,37 +285,85 @@ func importSchema(schema *CArrowSchema) (ret arrow.Field, err error) {
258285
}
259286

260287
if f[0] == '+' { // types with children
288+
if len(f) < 2 {
289+
return ret, fmt.Errorf("%w: invalid nested type format %q", arrow.ErrInvalid, f)
290+
}
261291
switch f[1] {
262292
case 'l': // list
293+
if f != "+l" {
294+
return ret, fmt.Errorf("%w: invalid list type format %q", arrow.ErrInvalid, f)
295+
}
296+
if len(childFields) != 1 {
297+
return ret, fmt.Errorf("%w: list type must have exactly 1 child", arrow.ErrInvalid)
298+
}
263299
dt = arrow.ListOfField(childFields[0])
264300
case 'L': // large list
301+
if f != "+L" {
302+
return ret, fmt.Errorf("%w: invalid large list type format %q", arrow.ErrInvalid, f)
303+
}
304+
if len(childFields) != 1 {
305+
return ret, fmt.Errorf("%w: large list type must have exactly 1 child", arrow.ErrInvalid)
306+
}
265307
dt = arrow.LargeListOfField(childFields[0])
266308
case 'v': // list view/large list view
309+
if (f != "+vl" && f != "+vL") || len(childFields) != 1 {
310+
return ret, fmt.Errorf("%w: invalid list view type format %q or child count %d", arrow.ErrInvalid, f, len(childFields))
311+
}
267312
switch f[2] {
268313
case 'l':
269314
dt = arrow.ListViewOfField(childFields[0])
270315
case 'L':
271316
dt = arrow.LargeListViewOfField(childFields[0])
317+
default:
318+
return ret, fmt.Errorf("%w: invalid list view type format %q", arrow.ErrInvalid, f)
272319
}
273320
case 'w': // fixed size list is w:# where # is the list size.
274-
listSize, err := strconv.Atoi(strings.Split(f, ":")[1])
321+
if len(childFields) != 1 {
322+
return ret, fmt.Errorf("%w: fixed-size list type must have exactly 1 child", arrow.ErrInvalid)
323+
}
324+
_, size, ok := strings.Cut(f, ":")
325+
if !ok {
326+
return ret, fmt.Errorf("%w: invalid fixed-size list format %q", arrow.ErrInvalid, f)
327+
}
328+
listSize, err := strconv.Atoi(size)
275329
if err != nil {
276-
return ret, err
330+
return ret, fmt.Errorf("%w: invalid fixed-size list format %q: %v", arrow.ErrInvalid, f, err)
331+
}
332+
if listSize <= 0 || int64(listSize) > 1<<31-1 {
333+
return ret, fmt.Errorf("%w: fixed-size list size must be in the range [1, %d]: %d", arrow.ErrInvalid, 1<<31-1, listSize)
277334
}
278335

279336
dt = arrow.FixedSizeListOfField(int32(listSize), childFields[0])
280337
case 's': // struct
338+
if f != "+s" {
339+
return ret, fmt.Errorf("%w: invalid struct type format %q", arrow.ErrInvalid, f)
340+
}
281341
dt = arrow.StructOf(childFields...)
282342
case 'r': // run-end encoded
343+
if f != "+r" {
344+
return ret, fmt.Errorf("%w: invalid run-end encoded type format %q", arrow.ErrInvalid, f)
345+
}
283346
if len(childFields) != 2 {
284347
return ret, fmt.Errorf("%w: run-end encoded arrays must have 2 children", arrow.ErrInvalid)
285348
}
286349
dt = arrow.RunEndEncodedOf(childFields[0].Type, childFields[1].Type)
287350
case 'm': // map type is basically a list of structs.
288-
st := childFields[0].Type.(*arrow.StructType)
351+
if f != "+m" {
352+
return ret, fmt.Errorf("%w: invalid map type format %q", arrow.ErrInvalid, f)
353+
}
354+
if len(childFields) != 1 {
355+
return ret, fmt.Errorf("%w: map type must have exactly 1 child", arrow.ErrInvalid)
356+
}
357+
st, ok := childFields[0].Type.(*arrow.StructType)
358+
if !ok || st.NumFields() != 2 {
359+
return ret, fmt.Errorf("%w: map child must be a struct with exactly 2 fields", arrow.ErrInvalid)
360+
}
289361
dt = arrow.MapOf(st.Field(0).Type, st.Field(1).Type)
290362
dt.(*arrow.MapType).KeysSorted = (schema.flags & C.ARROW_FLAG_MAP_KEYS_SORTED) != 0
291363
case 'u': // union
364+
if len(f) < 3 {
365+
return ret, fmt.Errorf("%w: invalid union type format %q", arrow.ErrInvalid, f)
366+
}
292367
var mode arrow.UnionMode
293368
switch f[2] {
294369
case 'd':

arrow/cdata/cdata_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"errors"
3030
"fmt"
3131
"io"
32+
"math"
3233
"runtime"
3334
"runtime/cgo"
3435
"sync"
@@ -106,6 +107,62 @@ func TestSimpleArrayAndSchema(t *testing.T) {
106107
}
107108
}
108109

110+
func TestImportSchemaRejectsMalformedFormats(t *testing.T) {
111+
for _, format := range []string{"", "+", "+v", "+l", "+w", "+m", "+u"} {
112+
t.Run(format, func(t *testing.T) {
113+
schema := testPrimitive(format)
114+
_, err := ImportCArrowField(&schema)
115+
require.ErrorIs(t, err, arrow.ErrInvalid)
116+
require.True(t, schemaIsReleased(&schema))
117+
})
118+
}
119+
}
120+
121+
func TestImportSchemaRejectsInvalidNestedFormats(t *testing.T) {
122+
for _, format := range []string{"+vx", "+vlx", "+vLx", "+lx", "+Lx", "+w:0", "+w:-1", "+w:2147483648"} {
123+
t.Run(format, func(t *testing.T) {
124+
schemas := testNested([]string{format, "i"}, []string{"", "item"}, []bool{true})
125+
defer freeMallocedSchemas(schemas)
126+
127+
top := (*[1]*CArrowSchema)(unsafe.Pointer(schemas))[0]
128+
_, err := ImportCArrowField(top)
129+
require.ErrorIs(t, err, arrow.ErrInvalid)
130+
require.True(t, schemaIsReleased(top))
131+
})
132+
}
133+
134+
schema := testPrimitive("+sx")
135+
_, err := ImportCArrowField(&schema)
136+
require.ErrorIs(t, err, arrow.ErrInvalid)
137+
require.True(t, schemaIsReleased(&schema))
138+
}
139+
140+
func TestImportSchemaRejectsInvalidFixedSizeBinaryWidths(t *testing.T) {
141+
for _, format := range []string{"w:0", "w:-1"} {
142+
t.Run(format, func(t *testing.T) {
143+
schema := testPrimitive(format)
144+
_, err := ImportCArrowField(&schema)
145+
require.ErrorIs(t, err, arrow.ErrInvalid)
146+
require.True(t, schemaIsReleased(&schema))
147+
})
148+
}
149+
}
150+
151+
func TestImportCArrowSchemaRejectsPrimitiveTopLevel(t *testing.T) {
152+
schema := testPrimitive("i")
153+
_, err := ImportCArrowSchema(&schema)
154+
require.ErrorIs(t, err, arrow.ErrInvalid)
155+
require.True(t, schemaIsReleased(&schema))
156+
}
157+
158+
func TestImportSchemaRejectsOversizedChildCount(t *testing.T) {
159+
schema := testPrimitive("+s")
160+
setCSchemaChildCount(&schema, math.MaxInt64)
161+
_, err := ImportCArrowField(&schema)
162+
require.ErrorIs(t, err, arrow.ErrInvalid)
163+
require.True(t, schemaIsReleased(&schema))
164+
}
165+
109166
func TestPrimitiveSchemas(t *testing.T) {
110167
tests := []struct {
111168
typ arrow.DataType

arrow/cdata/cdata_test_framework.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ func schemaIsReleased(s *CArrowSchema) bool {
109109
return C.ArrowSchemaIsReleased(s) == 1
110110
}
111111

112+
func setCSchemaChildCount(s *CArrowSchema, n int64) {
113+
s.n_children = C.int64_t(n)
114+
}
115+
112116
func getMetadataKeys() ([]string, []string) {
113117
return []string{"key1", "key2"}, []string{"key"}
114118
}

arrow/cdata/interface.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package cdata
2222
import (
2323
"context"
2424
"errors"
25+
"fmt"
2526
"unsafe"
2627

2728
"github.com/apache/arrow-go/v18/arrow"
@@ -58,7 +59,12 @@ func ImportCArrowSchema(out *CArrowSchema) (*arrow.Schema, error) {
5859
return nil, err
5960
}
6061

61-
return arrow.NewSchema(ret.Type.(*arrow.StructType).Fields(), &ret.Metadata), nil
62+
structType, ok := ret.Type.(*arrow.StructType)
63+
if !ok {
64+
return nil, fmt.Errorf("%w: record batch schema must have a top-level struct type", arrow.ErrInvalid)
65+
}
66+
67+
return arrow.NewSchema(structType.Fields(), &ret.Metadata), nil
6268
}
6369

6470
// ImportCArrayWithType takes a pointer to a C Data ArrowArray and interprets the values

0 commit comments

Comments
 (0)