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]