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


##########
parquet/variant/variant.go:
##########
@@ -556,20 +557,331 @@ 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 validationChild struct {
+       value        []byte
+       index        int
+       start        uint64
+       expectedSize uint64
+}
+
+type validationRange struct {
+       start uint64
+       end   uint64
+       field int
+}
+
+type validationFrame struct {
+       value       []byte
+       kind        BasicType
+       size        int
+       dataSize    uint64
+       children    []validationChild
+       ranges      []validationRange
+       nextChild   int
+       pending     validationChild
+       initialized bool
+       compound    bool
+}
+
+// validateValue walks compound values with an explicit stack so valid values
+// are not limited by the Go call stack or an implementation-defined nesting
+// depth.
+func validateValue(meta Metadata, value []byte) (int, error) {
+       stack := []validationFrame{{value: value}}
+       var (
+               resultSize int
+               resultErr  error
+               hasResult  bool
+       )
+
+       for len(stack) > 0 {
+               frame := &stack[len(stack)-1]
+               if hasResult {
+                       child := frame.pending
+                       hasResult = false
+
+                       if resultErr != nil {
+                               switch frame.kind {
+                               case BasicArray:
+                                       return 0, fmt.Errorf("invalid variant 
value: array element %d: %w", child.index, resultErr)
+                               case BasicObject:
+                                       return 0, fmt.Errorf("invalid variant 
value: object field %d: %w", child.index, resultErr)
+                               default:
+                                       return 0, resultErr
+                               }
+                       }
+
+                       switch frame.kind {
+                       case BasicArray:
+                               if uint64(resultSize) != child.expectedSize {
+                                       return 0, fmt.Errorf("invalid variant 
value: array element %d has trailing bytes", child.index)
+                               }
+                       case BasicObject:
+                               end := child.start + uint64(resultSize)
+                               if end > frame.dataSize {
+                                       return 0, fmt.Errorf("invalid variant 
value: object field %d extends beyond data", child.index)
+                               }
+                               frame.ranges = append(frame.ranges, 
validationRange{
+                                       start: child.start,
+                                       end:   end,
+                                       field: child.index,
+                               })
+                       }
+                       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 frame.compound {
+                       if frame.nextChild < len(frame.children) {
+                               child := frame.children[frame.nextChild]
+                               frame.nextChild++
+                               frame.pending = child
+                               stack = append(stack, validationFrame{value: 
child.value})
+                               continue
+                       }
+
+                       if err := finishValidationFrame(frame); err != nil {
+                               stack = stack[:len(stack)-1]
+                               if len(stack) == 0 {
+                                       return 0, err
+                               }
+                               resultErr = err
+                               hasResult = true
+                               continue
+                       }
+               }
+
+               resultSize = 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 finishValidationFrame(frame *validationFrame) error {
+       if frame.kind != BasicObject {
+               return nil
+       }
+
+       slices.SortFunc(frame.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 frame.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 != 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 = basicTypeFromHeader(frame.value[0])
+       switch 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 = want
+       case BasicObject:
+               frame.compound = true
+               return prepareObjectValidationFrame(meta, frame)
+       case BasicArray:
+               frame.compound = true
+               return prepareArrayValidationFrame(frame)
+       case BasicPrimitive:
+               var err error
+               frame.size, err = validatePrimitiveValue(frame.value)
+               return err
+       default:
+               return fmt.Errorf("invalid variant value: unknown basic type 
%d", 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))
+       }
+
+       offsets := make([]uint32, int(numElements)+1)
+       for i := range offsets {
+               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 < offsets[i-1] {
+                       return fmt.Errorf("invalid variant value: array offsets 
are not monotonic")
+               }
+               if dataStart+uint64(offset) > uint64(len(value)) || 
dataStart+uint64(offset) > math.MaxUint32 {
+                       return fmt.Errorf("invalid variant value: array offset 
%d is out of range", offset)
+               }
+               offsets[i] = offset
+       }
+
+       frame.children = make([]validationChild, 0, len(offsets)-1)

Review Comment:
   **Blocking:** Validation eagerly allocates a `validationChild` for every 
child; objects subsequently allocate another `validationRange` per field. This 
happens on every `NewWithMetadata`, including the Parquet row-read path. A 
direct 40-field benchmark changed from approximately `4.9 ns/op, 0 B/op, 0 
allocs/op` on main to `1.58 µs/op, 5576 B/op, 10 allocs/op`. Wide or deeply 
nested untrusted values therefore create substantial memory amplification and 
potential OOM pressure. Please iterate child offsets incrementally or otherwise 
bound/reuse validator state, and add representative allocation benchmarks.



##########
parquet/variant/variant.go:
##########
@@ -512,25 +513,25 @@ func NewWithMetadata(meta Metadata, value []byte) (Value, 
error) {
        if len(value) == 0 {
                return Value{}, errors.New("invalid variant value: empty")
        }
-       if err := validateScalarValue(value); err != nil {
+       if err := validateValueBytes(meta, value); err != nil {
                return Value{}, err
        }
 
        return Value{value: value, meta: meta}, nil
 }
 
-func validateScalarValue(value []byte) error {
-       if basicTypeFromHeader(value[0]) == BasicShortString {
-               want := 1 + int(value[0]>>basicTypeBits)
-               if len(value) < want {
-                       return fmt.Errorf("invalid variant value: short string 
requires %d bytes, got %d", want, len(value))
-               }
-               return nil
+func validateValueBytes(meta Metadata, value []byte) error {
+       size, err := validateValue(meta, value)
+       if err != nil {
+               return err
        }
-       if basicTypeFromHeader(value[0]) != BasicPrimitive {
-               return nil
+       if size != len(value) {

Review Comment:
   **Blocking:** The exact-length check rejects byte slices returned by the 
public indexed accessors. `ArrayValue.Value`, `ObjectValue.ValueByKey`, and 
`ObjectValue.FieldAt` return an untrimmed slice extending through the parent’s 
remaining data, unlike the `Values` iterators. Reproduction: parse 
`{"a":1,"b":2}`, retrieve `"a"` with `ValueByKey`, then call 
`NewWithMetadata(v.Metadata(), field.Value.Bytes())`; this PR returns `invalid 
variant value: trailing bytes`. Please trim values returned by all indexed 
accessors to their encoded size and add round-trip tests for arrays and objects.



-- 
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