zeroshade commented on code in PR #1126:
URL: https://github.com/apache/arrow-go/pull/1126#discussion_r3866486581


##########
parquet/variant/variant.go:
##########
@@ -556,23 +556,406 @@ func validateScalarValue(value []byte) error {
                want = 17
        case PrimitiveBinary, PrimitiveString:
                if len(value) < 5 {
-                       return fmt.Errorf("invalid variant value: %s length 
prefix requires 5 bytes, got %d", primitiveType, len(value))
+                       return 0, fmt.Errorf("invalid variant value: %s length 
prefix requires 5 bytes, got %d", primitiveType, len(value))
                }
                dataLen := uint64(binary.LittleEndian.Uint32(value[1:5]))
                if dataLen > uint64(len(value)-5) {
-                       return fmt.Errorf("invalid variant value: %s data 
requires %d bytes, got %d", primitiveType, dataLen, len(value)-5)
+                       return 0, fmt.Errorf("invalid variant value: %s data 
requires %d bytes, got %d", primitiveType, dataLen, len(value)-5)
                }
-               return nil
+               return 5 + int(dataLen), nil
        default:
-               return fmt.Errorf("invalid variant value: unknown primitive 
type %d", primitiveType)
+               return 0, fmt.Errorf("invalid variant value: unknown primitive 
type %d", primitiveType)
        }
 
        if len(value) < want {
-               return fmt.Errorf("invalid variant value: %s requires %d bytes, 
got %d", primitiveType, want, len(value))
+               return 0, fmt.Errorf("invalid variant value: %s requires %d 
bytes, got %d", primitiveType, want, len(value))
        }
+       return want, nil
+}
+
+type validationRange struct {
+       start uint64
+       end   uint64
+       field int
+}
+
+type validationFrame struct {
+       value               []byte
+       size                uint64
+       dataSize            uint32
+       dataStart           uint32
+       offsetStart         uint32
+       numChildren         uint32
+       nextChild           uint32
+       pendingIndex        uint32
+       pendingStart        uint32
+       pendingExpectedSize uint32
+       rangeStart          uint32
+       offsetSize          uint8
+       kind                uint8
+       initialized         bool
+}
+
+const (
+       validationStackInlineCapacity = 32
+       // Values that exceed the inline stack commonly need only a modest 
amount
+       // of additional depth, so avoid allocating the maximum stack for them.
+       validationStackIntermediateCapacity = 128
+       // The root value is at depth zero, so the stack needs one more frame 
than
+       // the maximum allowed nesting depth. Keeping this storage fixed 
prevents
+       // untrusted values from growing the validation stack on the heap.
+       validationStackCapacity       = maxValidationDepth + 1
+       validationRangeInlineCapacity = 64
+)
+
+// validateValue walks compound values with an explicit stack so valid values
+// do not consume the Go call stack. Nesting is bounded to keep validation
+// memory usage independent of attacker-controlled input depth.
+func validateValue(meta Metadata, value []byte) (int, error) {
+       var stackStorage [validationStackInlineCapacity]validationFrame
+       stack := stackStorage[:1]
+       stack[0].value = value
+
+       var rangeStorage [validationRangeInlineCapacity]validationRange
+       ranges := rangeStorage[:0]
+       return validateValueLoop(meta, value, stack, ranges, 0)
+}
+
+func validateValueLoop(meta Metadata, value []byte, stack []validationFrame, 
ranges []validationRange, rangeTop int) (int, error) {
+       var (
+               resultSize int
+               resultErr  error
+               hasResult  bool
+       )
+
+       for len(stack) > 0 {
+               frame := &stack[len(stack)-1]
+               if hasResult {
+                       hasResult = false
+
+                       if resultErr != nil {
+                               switch BasicType(frame.kind) {
+                               case BasicArray:
+                                       return 0, fmt.Errorf("invalid variant 
value: array element %d: %w", frame.pendingIndex, resultErr)
+                               case BasicObject:
+                                       return 0, fmt.Errorf("invalid variant 
value: object field %d: %w", frame.pendingIndex, resultErr)
+                               default:
+                                       return 0, resultErr
+                               }
+                       }
+
+                       switch BasicType(frame.kind) {
+                       case BasicArray:
+                               if uint64(resultSize) != 
uint64(frame.pendingExpectedSize) {
+                                       return 0, fmt.Errorf("invalid variant 
value: array element %d has trailing bytes", frame.pendingIndex)
+                               }
+                       case BasicObject:
+                               end := uint64(frame.pendingStart) + 
uint64(resultSize)
+                               if end > uint64(frame.dataSize) {
+                                       return 0, fmt.Errorf("invalid variant 
value: object field %d extends beyond data", frame.pendingIndex)
+                               }
+                               if rangeTop < len(ranges) {
+                                       ranges[rangeTop] = validationRange{
+                                               start: 
uint64(frame.pendingStart),
+                                               end:   end,
+                                               field: int(frame.pendingIndex),
+                                       }
+                               } else {
+                                       ranges = append(ranges, validationRange{
+                                               start: 
uint64(frame.pendingStart),
+                                               end:   end,
+                                               field: int(frame.pendingIndex),
+                                       })
+                               }
+                               rangeTop++
+                       }
+                       continue
+               }
+
+               if !frame.initialized {
+                       frame.initialized = true
+                       if err := prepareValidationFrame(meta, frame); err != 
nil {
+                               stack = stack[:len(stack)-1]
+                               if len(stack) == 0 {
+                                       return 0, err
+                               }
+                               resultErr = err
+                               hasResult = true
+                               continue
+                       }
+                       if BasicType(frame.kind) == BasicObject {
+                               frame.rangeStart = uint32(rangeTop)
+                       }
+               }
+
+               if frame.kind == uint8(BasicArray) || frame.kind == 
uint8(BasicObject) {
+                       if frame.nextChild < frame.numChildren {
+                               if len(stack) == validationStackCapacity {
+                                       return 0, fmt.Errorf("invalid variant 
value: maximum nesting depth exceeded")
+                               }
+                               if len(stack) == cap(stack) {
+                                       if cap(stack) == 
validationStackInlineCapacity {
+                                               return 
validateValueIntermediate(meta, value, stack, ranges, rangeTop)
+                                       }
+                                       return validateValueDeep(meta, value, 
stack, ranges, rangeTop)
+                               }
+
+                               child, index, start, expectedSize, err := 
nextValidationChild(frame)
+                               if err != nil {
+                                       stack = stack[:len(stack)-1]
+                                       if len(stack) == 0 {
+                                               return 0, err
+                                       }
+                                       resultErr = err
+                                       hasResult = true
+                                       continue
+                               }
+
+                               frame.nextChild++
+                               frame.pendingIndex = uint32(index)
+                               frame.pendingStart = uint32(start)
+                               frame.pendingExpectedSize = uint32(expectedSize)
+                               stack = append(stack, validationFrame{value: 
child})
+                               continue
+                       }
+
+                       if err := finishValidationFrame(frame, 
ranges[int(frame.rangeStart):rangeTop]); err != nil {
+                               if BasicType(frame.kind) == BasicObject {
+                                       rangeTop = int(frame.rangeStart)
+                               }
+                               stack = stack[:len(stack)-1]
+                               if len(stack) == 0 {
+                                       return 0, err
+                               }
+                               resultErr = err
+                               hasResult = true
+                               continue
+                       }
+                       if BasicType(frame.kind) == BasicObject {
+                               rangeTop = int(frame.rangeStart)
+                       }
+               }
+
+               resultSize = int(frame.size)
+               stack = stack[:len(stack)-1]
+               if len(stack) == 0 {
+                       return resultSize, nil
+               }
+               hasResult = true
+       }
+
+       return 0, errors.New("invalid variant value: validation stack 
exhausted")
+}
+
+func validateValueIntermediate(meta Metadata, value []byte, initialStack 
[]validationFrame, ranges []validationRange, rangeTop int) (int, error) {
+       var stackStorage [validationStackIntermediateCapacity]validationFrame
+       stack := stackStorage[:len(initialStack)]
+       copy(stack, initialStack)
+       return validateValueLoop(meta, value, stack, ranges, rangeTop)
+}
+
+func validateValueDeep(meta Metadata, value []byte, initialStack 
[]validationFrame, ranges []validationRange, rangeTop int) (int, error) {
+       var stackStorage [validationStackCapacity]validationFrame
+       stack := stackStorage[:len(initialStack)]
+       copy(stack, initialStack)
+       return validateValueLoop(meta, value, stack, ranges, rangeTop)
+}
+
+func finishValidationFrame(frame *validationFrame, ranges []validationRange) 
error {
+       if BasicType(frame.kind) != BasicObject {
+               return nil
+       }
+
+       slices.SortFunc(ranges, func(a, b validationRange) int {
+               switch {
+               case a.start < b.start:
+                       return -1
+               case a.start > b.start:
+                       return 1
+               default:
+                       return 0
+               }
+       })
+
+       var (
+               next          uint64
+               previousField int
+       )
+       for _, child := range ranges {
+               switch {
+               case child.start < next:
+                       return fmt.Errorf("invalid variant value: object fields 
%d and %d overlap", previousField, child.field)
+               case child.start > next:
+                       return fmt.Errorf("invalid variant value: object data 
has a gap before field %d", child.field)
+               }
+               next = child.end
+               previousField = child.field
+       }
+       if next != uint64(frame.dataSize) {
+               return fmt.Errorf("invalid variant value: object data has 
trailing bytes")
+       }
+       return nil
+}
+
+func prepareValidationFrame(meta Metadata, frame *validationFrame) error {
+       if len(frame.value) == 0 {
+               return errors.New("invalid variant value: empty")
+       }
+
+       frame.kind = uint8(basicTypeFromHeader(frame.value[0]))
+       switch BasicType(frame.kind) {
+       case BasicShortString:
+               want := 1 + int(frame.value[0]>>basicTypeBits)
+               if len(frame.value) < want {
+                       return fmt.Errorf("invalid variant value: short string 
requires %d bytes, got %d", want, len(frame.value))
+               }
+               frame.size = uint64(want)
+       case BasicObject:
+               return prepareObjectValidationFrame(meta, frame)
+       case BasicArray:
+               return prepareArrayValidationFrame(frame)
+       case BasicPrimitive:
+               size, err := validatePrimitiveValue(frame.value)
+               frame.size = uint64(size)
+               return err
+       default:
+               return fmt.Errorf("invalid variant value: unknown basic type 
%d", BasicType(frame.kind))
+       }
+       return nil
+}
+
+func prepareArrayValidationFrame(frame *validationFrame) error {
+       value := frame.value
+       typeInfo := value[0] >> basicTypeBits
+       offsetSize := uint8(typeInfo&0b11) + 1
+       isLarge := ((typeInfo >> 2) & 0x1) != 0
+
+       var (
+               numElements uint32
+               offsetStart uint64
+       )
+       if isLarge {
+               if len(value) < 5 {
+                       return fmt.Errorf("invalid variant value: array size 
requires 5 bytes, got %d", len(value))
+               }
+               numElements = readLEU32(value[1:5])
+               offsetStart = 5
+       } else {
+               if len(value) < 2 {
+                       return fmt.Errorf("invalid variant value: array size 
requires 2 bytes, got %d", len(value))
+               }
+               numElements = uint32(value[1])
+               offsetStart = 2
+       }
+
+       dataStart := offsetStart + (uint64(numElements)+1)*uint64(offsetSize)
+       if dataStart > uint64(len(value)) || dataStart > math.MaxUint32 {
+               return fmt.Errorf("invalid variant value: array offset table 
ends at %d, got %d bytes", dataStart, len(value))
+       }
+
+       var previousOffset uint32
+       for i := uint64(0); i <= uint64(numElements); i++ {
+               pos := offsetStart + uint64(i)*uint64(offsetSize)
+               offset := readLEU32(value[int(pos) : int(pos)+int(offsetSize)])
+               if i == 0 && offset != 0 {
+                       return fmt.Errorf("invalid variant value: array first 
offset must be zero, got %d", offset)
+               }
+               if i > 0 && offset < previousOffset {
+                       return fmt.Errorf("invalid variant value: array offsets 
are not monotonic")
+               }
+               if dataStart+uint64(offset) > uint64(len(value)) || 
dataStart+uint64(offset) > math.MaxUint32 {

Review Comment:
   **Blocking:** This treats `dataStart + offset` as though the complete 
encoded value were limited to `uint32`, but Variant's 4-byte field offsets are 
relative to the start of the fields region. A valid compound can therefore 
contain a `math.MaxUint32`-byte fields region plus its header and offset table. 
A sparse-mapped array with one max-sized binary child passes on the merge base 
but fails here with `array offset 4294967295 is out of range`. The equivalent 
object condition at line 907 has the same issue. Please retain total positions 
as `uint64` and compare them against `len(value)` without imposing this 
undocumented total-value limit.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to