This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 3b68521e3 fix(go): add configurable fieldCount and fieldDepth 
guardrails (#3620)
3b68521e3 is described below

commit 3b68521e36544192ddfc3d64f23bb96c47ba2088
Author: Ayush Kumar <[email protected]>
AuthorDate: Mon Apr 27 17:49:30 2026 +0530

    fix(go): add configurable fieldCount and fieldDepth guardrails (#3620)
    
    ## Why?
    Malicious payloads could specify a massive `fieldCount`, causing the
    runtime to attempt an unbounded memory allocation.
    Deeply nested schema definitions (like a LIST of LIST...) could trigger
    unbounded recursion, exceeding the goroutine stack limit and crashing
    the process.
    
    ## What does this PR do?
    Added a hard limit of 10,000 fields and a buffer-remaining check in
    decodeTypeDef to prevent massive slice allocations.
    Added a depth parameter to `readFieldType` and `readFieldTypeWithFlags`,
    capping nested schema definitions at a maximum depth of 64.
    
    ## Related issues
    Closes #3619
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 go/fory/fory.go          |  9 +++++++++
 go/fory/type_def.go      | 34 ++++++++++++++++++++++++----------
 go/fory/type_def_test.go | 41 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 74 insertions(+), 10 deletions(-)

diff --git a/go/fory/fory.go b/go/fory/fory.go
index 565dd1a5b..bd1dab65b 100644
--- a/go/fory/fory.go
+++ b/go/fory/fory.go
@@ -56,6 +56,7 @@ type Config struct {
        Compatible        bool // Schema evolution compatibility mode
        MaxCollectionSize int
        MaxBinarySize     int
+       MaxTypeFields     int
 }
 
 // defaultConfig returns the default configuration
@@ -66,6 +67,7 @@ func defaultConfig() Config {
                IsXlang:           false,
                MaxCollectionSize: 1_000_000,
                MaxBinarySize:     64 * 1024 * 1024,
+               MaxTypeFields:     10000,
        }
 }
 
@@ -119,6 +121,13 @@ func WithMaxBinarySize(size int) Option {
        }
 }
 
+// WithMaxTypeFields sets the maximum field count limit for schema definition 
deserialization
+func WithMaxTypeFields(size int) Option {
+       return func(f *Fory) {
+               f.config.MaxTypeFields = size
+       }
+}
+
 // ============================================================================
 // Fory - Main serialization instance
 // ============================================================================
diff --git a/go/fory/type_def.go b/go/fory/type_def.go
index bd55de459..d4dcf2bdd 100644
--- a/go/fory/type_def.go
+++ b/go/fory/type_def.go
@@ -726,25 +726,28 @@ func (b *BaseFieldType) getTypeInfoWithResolver(resolver 
*TypeResolver) (TypeInf
 
 // readFieldType reads field type info from the buffer according to the TypeId
 // This is called for top-level field types where flags are NOT embedded in 
the type ID
-func readFieldType(buffer *ByteBuffer, err *Error) (FieldType, error) {
+func readFieldType(buffer *ByteBuffer, depth int, maxDepth int, err *Error) 
(FieldType, error) {
+       if depth > maxDepth {
+               return nil, fmt.Errorf("schema type definition exceeds maximum 
nesting depth")
+       }
        typeId := buffer.ReadUint8(err)
        internalTypeId := TypeId(typeId)
 
        switch internalTypeId {
        case LIST, SET:
                // For nested types, flags ARE embedded in the type ID
-               elementType, etErr := readFieldTypeWithFlags(buffer, err)
+               elementType, etErr := readFieldTypeWithFlags(buffer, depth+1, 
maxDepth, err)
                if etErr != nil {
                        return nil, fmt.Errorf("failed to read element type: 
%w", etErr)
                }
                return NewCollectionFieldType(TypeId(typeId), elementType), nil
        case MAP:
                // For nested types, flags ARE embedded in the type ID
-               keyType, ktErr := readFieldTypeWithFlags(buffer, err)
+               keyType, ktErr := readFieldTypeWithFlags(buffer, depth+1, 
maxDepth, err)
                if ktErr != nil {
                        return nil, fmt.Errorf("failed to read key type: %w", 
ktErr)
                }
-               valueType, vtErr := readFieldTypeWithFlags(buffer, err)
+               valueType, vtErr := readFieldTypeWithFlags(buffer, depth+1, 
maxDepth, err)
                if vtErr != nil {
                        return nil, fmt.Errorf("failed to read value type: %w", 
vtErr)
                }
@@ -759,7 +762,10 @@ func readFieldType(buffer *ByteBuffer, err *Error) 
(FieldType, error) {
 
 // readFieldTypeWithFlags reads field type info where flags are embedded in 
the type ID
 // Format: (typeId << 2) | (nullable ? 0b10 : 0) | (trackingRef ? 0b1 : 0)
-func readFieldTypeWithFlags(buffer *ByteBuffer, err *Error) (FieldType, error) 
{
+func readFieldTypeWithFlags(buffer *ByteBuffer, depth int, maxDepth int, err 
*Error) (FieldType, error) {
+       if depth > maxDepth {
+               return nil, fmt.Errorf("schema type definition exceeds maximum 
nesting depth")
+       }
        rawValue := buffer.ReadVarUint32Small7(err)
        // Extract flags (lower 2 bits)
        // trackingRef := (rawValue & 0b1) != 0  // Not used currently
@@ -769,17 +775,17 @@ func readFieldTypeWithFlags(buffer *ByteBuffer, err 
*Error) (FieldType, error) {
 
        switch internalTypeId {
        case LIST, SET:
-               elementType, etErr := readFieldTypeWithFlags(buffer, err)
+               elementType, etErr := readFieldTypeWithFlags(buffer, depth+1, 
maxDepth, err)
                if etErr != nil {
                        return nil, fmt.Errorf("failed to read element type: 
%w", etErr)
                }
                return NewCollectionFieldType(TypeId(typeId), elementType), nil
        case MAP:
-               keyType, ktErr := readFieldTypeWithFlags(buffer, err)
+               keyType, ktErr := readFieldTypeWithFlags(buffer, depth+1, 
maxDepth, err)
                if ktErr != nil {
                        return nil, fmt.Errorf("failed to read key type: %w", 
ktErr)
                }
-               valueType, vtErr := readFieldTypeWithFlags(buffer, err)
+               valueType, vtErr := readFieldTypeWithFlags(buffer, depth+1, 
maxDepth, err)
                if vtErr != nil {
                        return nil, fmt.Errorf("failed to read value type: %w", 
vtErr)
                }
@@ -1485,6 +1491,9 @@ func decodeTypeDef(fory *Fory, buffer *ByteBuffer, header 
int64) (*TypeDef, erro
        if fieldCount == SmallNumFieldsThreshold {
                fieldCount += int(metaBuffer.ReadVarUint32(&metaErr))
        }
+       if fieldCount > fory.config.MaxTypeFields || fieldCount > 
metaBuffer.remaining() {
+               return nil, fmt.Errorf("field count exceeds maximum allowed 
limit or available buffer size")
+       }
        registeredByName := (metaHeaderByte & REGISTER_BY_NAME_FLAG) != 0
 
        // ReadData name or type ID according to the registerByName flag
@@ -1679,6 +1688,11 @@ field def layout as following:
 */
 func readFieldDef(typeResolver *TypeResolver, buffer *ByteBuffer) (FieldDef, 
error) {
        var bufErr Error
+       maxDepth := defaultConfig().MaxDepth
+       if typeResolver != nil && typeResolver.fory != nil {
+               maxDepth = typeResolver.fory.config.MaxDepth
+       }
+
        // ReadData field header
        headerByte := buffer.ReadByte(&bufErr)
        if bufErr.HasError() {
@@ -1700,7 +1714,7 @@ func readFieldDef(typeResolver *TypeResolver, buffer 
*ByteBuffer) (FieldDef, err
                }
 
                // Read field type
-               ft, err := readFieldType(buffer, &bufErr)
+               ft, err := readFieldType(buffer, 0, maxDepth, &bufErr)
                if err != nil {
                        return FieldDef{}, err
                }
@@ -1725,7 +1739,7 @@ func readFieldDef(typeResolver *TypeResolver, buffer 
*ByteBuffer) (FieldDef, err
        }
 
        // Read field type
-       ft, err := readFieldType(buffer, &bufErr)
+       ft, err := readFieldType(buffer, 0, maxDepth, &bufErr)
        if err != nil {
                return FieldDef{}, err
        }
diff --git a/go/fory/type_def_test.go b/go/fory/type_def_test.go
index df31e60d2..7e691ac46 100644
--- a/go/fory/type_def_test.go
+++ b/go/fory/type_def_test.go
@@ -301,3 +301,44 @@ func TestTypeDefNullableFields(t *testing.T) {
                }
        })
 }
+
+// TestTypeDefFieldCountOOMPanic verifies that decodeTypeDef rejects a crafted 
payload
+// whose fieldCount (2 billion) far exceeds the hard cap and available buffer 
bytes,
+// returning an error instead of performing the unbounded make([]FieldDef, 
fieldCount)
+// allocation that would OOM-crash the process.
+func TestTypeDefFieldCountOOMPanic(t *testing.T) {
+       fory := NewFory()
+       header := int64(HAS_FIELDS_META_FLAG | 8)
+
+       // metaHeaderByte value of 31 triggers the extended VarUint32 
field-count path.
+       buffer := NewByteBuffer(make([]byte, 0, 8))
+       buffer.WriteByte(31)
+       buffer.WriteVarUint32(2000000000)
+       buffer.WriteUint8(0)
+       buffer.WriteVarUint32(0)
+       buffer.SetReaderIndex(0)
+
+       _, err := decodeTypeDef(fory, buffer, header)
+       if err == nil {
+               t.Fatal("expected error for oversized fieldCount, got nil")
+       }
+}
+
+// TestTypeDefNestedRecursionStackOverflowPanic verifies that 
readFieldTypeWithFlags
+// rejects a crafted payload with 20 million nested LIST types, returning an 
error
+// at depth 64 instead of recursing until a goroutine stack overflow crashes 
the process.
+func TestTypeDefNestedRecursionStackOverflowPanic(t *testing.T) {
+       depth := 20000000
+       buffer := NewByteBuffer(make([]byte, 0, depth*2))
+       for i := 0; i < depth; i++ {
+               buffer.WriteVarUint32Small7(uint32(LIST) << 2)
+       }
+       buffer.WriteVarUint32Small7(uint32(INT32) << 2)
+       buffer.SetReaderIndex(0)
+
+       bufErr := &Error{}
+       _, err := readFieldTypeWithFlags(buffer, 0, defaultConfig().MaxDepth, 
bufErr)
+       if err == nil {
+               t.Fatal("expected error for excessive nesting depth, got nil")
+       }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to