From fe80c0647f7eb32e8ad9f1b8231dd41247714428 Mon Sep 17 00:00:00 2001 From: Zakir Date: Thu, 26 Feb 2026 01:36:01 +0530 Subject: [PATCH 1/5] feat(go): add MaxStringBytes/MaxCollectionSize/MaxMapSize to Config Add three opt-in guardrail fields to Config and three corresponding WithXxx option functions. All default to 0 (unlimited), preserving full backward compatibility. Enforcement in read paths follows in subsequent commits. --- go/fory/fory.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/go/fory/fory.go b/go/fory/fory.go index 09a0e3c6d2..797ed7c7db 100644 --- a/go/fory/fory.go +++ b/go/fory/fory.go @@ -54,6 +54,9 @@ type Config struct { MaxDepth int IsXlang bool Compatible bool // Schema evolution compatibility mode + MaxStringBytes int + MaxCollectionSize int + MaxMapSize int } // defaultConfig returns the default configuration @@ -101,6 +104,31 @@ func WithCompatible(enabled bool) Option { } } +// WithMaxStringBytes sets the maximum allowed byte length for a single +// deserialized string. 0 (default) means no limit. +func WithMaxStringBytes(n int) Option { + return func(f *Fory) { + f.config.MaxStringBytes = n + } +} + +// WithMaxCollectionSize sets the maximum allowed element count for a single +// deserialized slice or list. 0 (default) means no limit. +func WithMaxCollectionSize(n int) Option { + return func(f *Fory) { + f.config.MaxCollectionSize = n + } +} + +// WithMaxMapSize sets the maximum allowed entry count for a single +// deserialized map. 0 (default) means no limit. +func WithMaxMapSize(n int) Option { + return func(f *Fory) { + f.config.MaxMapSize = n + } +} + + // ============================================================================ // Fory - Main serialization instance // ============================================================================ From fc05b92633453bfbf352155a58c17820cf633b2a Mon Sep 17 00:00:00 2001 From: Zakir Date: Thu, 26 Feb 2026 02:53:18 +0530 Subject: [PATCH 2/5] complete changes + test file --- go/fory/fory.go | 4 + go/fory/limits_test.go | 238 +++++++++++++++++++++++++++++++++++++ go/fory/map.go | 1 + go/fory/map_primitive.go | 94 +++++++++++---- go/fory/reader.go | 30 ++++- go/fory/slice.go | 2 + go/fory/slice_primitive.go | 4 +- go/fory/string.go | 12 +- 8 files changed, 359 insertions(+), 26 deletions(-) create mode 100644 go/fory/limits_test.go diff --git a/go/fory/fory.go b/go/fory/fory.go index 797ed7c7db..efb63ae783 100644 --- a/go/fory/fory.go +++ b/go/fory/fory.go @@ -184,6 +184,10 @@ func New(opts ...Option) *Fory { f.readCtx.refResolver = f.refResolver f.readCtx.compatible = f.config.Compatible f.readCtx.xlang = f.config.IsXlang + f.readCtx.maxStringBytes = f.config.MaxStringBytes + f.readCtx.maxCollectionSize = f.config.MaxCollectionSize + f.readCtx.maxMapSize = f.config.MaxMapSize + return f } diff --git a/go/fory/limits_test.go b/go/fory/limits_test.go new file mode 100644 index 0000000000..a4214431c3 --- /dev/null +++ b/go/fory/limits_test.go @@ -0,0 +1,238 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package fory + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// ============================================================================ +// MaxStringBytes +// ============================================================================ + +func TestMaxStringBytesBlocksOversizedString(t *testing.T) { + f := NewFory(WithXlang(true), WithMaxStringBytes(5)) + + long := strings.Repeat("a", 20) // 20 bytes > limit 5 + data, err := f.Marshal(long) + require.NoError(t, err) // write path has no limit + + var result string + err = f.Unmarshal(data, &result) + require.Error(t, err, "expected error: string exceeds MaxStringBytes") +} + +func TestMaxStringBytesAllowsExactLimit(t *testing.T) { + f := NewFory(WithXlang(true), WithMaxStringBytes(5)) + + s := "hello" // exactly 5 bytes — must NOT be rejected (> not >=) + data, err := f.Marshal(s) + require.NoError(t, err) + + var result string + require.NoError(t, f.Unmarshal(data, &result)) + require.Equal(t, s, result) +} + +func TestMaxStringBytesAllowsWithinLimit(t *testing.T) { + f := NewFory(WithXlang(true), WithMaxStringBytes(10)) + + s := "hi" // 2 bytes, well within limit + data, err := f.Marshal(s) + require.NoError(t, err) + + var result string + require.NoError(t, f.Unmarshal(data, &result)) + require.Equal(t, s, result) +} + +func TestMaxStringBytesZeroMeansNoLimit(t *testing.T) { + f := NewFory(WithXlang(true)) // default 0 = no limit + + long := strings.Repeat("x", 100_000) + data, err := f.Marshal(long) + require.NoError(t, err) + + var result string + require.NoError(t, f.Unmarshal(data, &result)) + require.Equal(t, long, result) +} + +// ============================================================================ +// MaxCollectionSize +// ============================================================================ + +func TestMaxCollectionSizeBlocksOversizedSlice(t *testing.T) { + f := NewFory(WithXlang(true), WithMaxCollectionSize(3)) + + s := []string{"a", "b", "c", "d", "e"} // 5 elements > limit 3 + data, err := f.Marshal(s) + require.NoError(t, err) + + var result []string + err = f.Unmarshal(data, &result) + require.Error(t, err, "expected error: slice exceeds MaxCollectionSize") +} + +func TestMaxCollectionSizeAllowsWithinLimit(t *testing.T) { + f := NewFory(WithXlang(true), WithMaxCollectionSize(5)) + + s := []string{"a", "b", "c"} // 3 elements, within limit 5 + data, err := f.Marshal(s) + require.NoError(t, err) + + var result []string + require.NoError(t, f.Unmarshal(data, &result)) + require.Equal(t, s, result) +} + +func TestMaxCollectionSizeAllowsExactLimit(t *testing.T) { + f := NewFory(WithXlang(true), WithMaxCollectionSize(3)) + + s := []string{"a", "b", "c"} // exactly 3 — must NOT be rejected + data, err := f.Marshal(s) + require.NoError(t, err) + + var result []string + require.NoError(t, f.Unmarshal(data, &result)) + require.Equal(t, s, result) +} + +func TestMaxCollectionSizeZeroMeansNoLimit(t *testing.T) { + f := NewFory(WithXlang(true)) // default 0 = no limit + + s := make([]int32, 10_000) + data, err := f.Marshal(s) + require.NoError(t, err) + + var result []int32 + require.NoError(t, f.Unmarshal(data, &result)) + require.Equal(t, len(s), len(result)) +} + +// ============================================================================ +// MaxMapSize +// ============================================================================ + +func TestMaxMapSizeBlocksOversizedMap(t *testing.T) { + f := NewFory(WithXlang(true), WithMaxMapSize(2)) + + m := map[string]string{"k1": "v1", "k2": "v2", "k3": "v3"} // 3 entries > limit 2 + data, err := f.Marshal(m) + require.NoError(t, err) + + var result map[string]string + err = f.Unmarshal(data, &result) + require.Error(t, err, "expected error: map exceeds MaxMapSize") +} + +func TestMaxMapSizeAllowsWithinLimit(t *testing.T) { + f := NewFory(WithXlang(true), WithMaxMapSize(5)) + + m := map[string]string{"k1": "v1", "k2": "v2"} + data, err := f.Marshal(m) + require.NoError(t, err) + + var result map[string]string + require.NoError(t, f.Unmarshal(data, &result)) + require.Equal(t, m, result) +} + +func TestMaxMapSizeAllowsExactLimit(t *testing.T) { + f := NewFory(WithXlang(true), WithMaxMapSize(2)) + + m := map[string]string{"k1": "v1", "k2": "v2"} // exactly 2 — must NOT be rejected + data, err := f.Marshal(m) + require.NoError(t, err) + + var result map[string]string + require.NoError(t, f.Unmarshal(data, &result)) + require.Equal(t, m, result) +} + +func TestMaxMapSizeZeroMeansNoLimit(t *testing.T) { + f := NewFory(WithXlang(true)) // default 0 = no limit + + m := make(map[string]string, 1000) + for i := 0; i < 1000; i++ { + m[fmt.Sprintf("k%d", i)] = "v" + } + data, err := f.Marshal(m) + require.NoError(t, err) + + var result map[string]string + require.NoError(t, f.Unmarshal(data, &result)) + require.Equal(t, 1000, len(result)) +} + +// ============================================================================ +// Combined limits +// ============================================================================ + +func TestCombinedLimitsStringInsideSlice(t *testing.T) { + // Slice size is within limit, but one element string is too long + f := NewFory(WithXlang(true), WithMaxCollectionSize(10), WithMaxStringBytes(3)) + + s := []string{"ab", "cd", "this-is-too-long"} // third element 16 bytes > limit 3 + data, err := f.Marshal(s) + require.NoError(t, err) + + var result []string + err = f.Unmarshal(data, &result) + require.Error(t, err) +} + +func TestCombinedLimitsCollectionFiresBeforeString(t *testing.T) { + // Collection limit fires before any string element is read + f := NewFory(WithXlang(true), WithMaxCollectionSize(2), WithMaxStringBytes(1000)) + + s := []string{"a", "b", "c", "d"} // 4 elements > collection limit 2 + data, err := f.Marshal(s) + require.NoError(t, err) + + var result []string + err = f.Unmarshal(data, &result) + require.Error(t, err) +} + +func TestCombinedLimitsAllWithinBounds(t *testing.T) { + // All limits set, all values within bounds — must succeed end-to-end + f := NewFory(WithXlang(true), + WithMaxStringBytes(20), + WithMaxCollectionSize(10), + WithMaxMapSize(10), + ) + + s := []string{"hello", "world"} + data, err := f.Marshal(s) + require.NoError(t, err) + var sliceResult []string + require.NoError(t, f.Unmarshal(data, &sliceResult)) + require.Equal(t, s, sliceResult) + + m := map[string]string{"k1": "v1", "k2": "v2"} + data, err = f.Marshal(m) + require.NoError(t, err) + var mapResult map[string]string + require.NoError(t, f.Unmarshal(data, &mapResult)) + require.Equal(t, m, mapResult) +} diff --git a/go/fory/map.go b/go/fory/map.go index f2489601f3..91dc0bcd24 100644 --- a/go/fory/map.go +++ b/go/fory/map.go @@ -306,6 +306,7 @@ func (s mapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { refResolver.Reference(value) size := int(buf.ReadVarUint32(ctxErr)) + ctx.checkMapSize(size) if size == 0 || ctx.HasError() { return } diff --git a/go/fory/map_primitive.go b/go/fory/map_primitive.go index 21a4bd7b5d..0cfbbd076f 100644 --- a/go/fory/map_primitive.go +++ b/go/fory/map_primitive.go @@ -69,8 +69,13 @@ func writeMapStringString(buf *ByteBuffer, m map[string]string, hasGenerics bool } // readMapStringString reads map[string]string using chunk protocol -func readMapStringString(buf *ByteBuffer, err *Error) map[string]string { +func readMapStringString(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]string { size := int(buf.ReadVarUint32(err)) +if maxMapSize > 0 && size > maxMapSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxMapSize)) + return nil +} result := make(map[string]string, size) if size == 0 { return result @@ -94,7 +99,8 @@ func readMapStringString(buf *ByteBuffer, err *Error) map[string]string { if !valueDeclared { buf.ReadUint8(err) // skip value type } - v := readString(buf, err) + v := readString(buf, err, maxStringBytes) + result[""] = v // empty string as null key size-- continue @@ -104,7 +110,8 @@ func readMapStringString(buf *ByteBuffer, err *Error) map[string]string { if !keyDeclared { buf.ReadUint8(err) // skip key type } - k := readString(buf, err) + k := readString(buf, err, maxStringBytes) + result[k] = "" // empty string as null value size-- continue @@ -123,8 +130,10 @@ func readMapStringString(buf *ByteBuffer, err *Error) map[string]string { // ReadData chunk entries for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err) - v := readString(buf, err) + k := readString(buf, err, maxStringBytes) + + v := readString(buf, err, maxStringBytes) + result[k] = v size-- } @@ -172,8 +181,13 @@ func writeMapStringInt64(buf *ByteBuffer, m map[string]int64, hasGenerics bool) } // readMapStringInt64 reads map[string]int64 using chunk protocol -func readMapStringInt64(buf *ByteBuffer, err *Error) map[string]int64 { +func readMapStringInt64(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]int64 { size := int(buf.ReadVarUint32(err)) +if maxMapSize > 0 && size > maxMapSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxMapSize)) + return nil +} result := make(map[string]int64, size) if size == 0 { return result @@ -197,7 +211,8 @@ func readMapStringInt64(buf *ByteBuffer, err *Error) map[string]int64 { buf.ReadUint8(err) } for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err) + k := readString(buf, err, maxStringBytes) + v := buf.ReadVarint64(err) result[k] = v size-- @@ -246,8 +261,13 @@ func writeMapStringInt32(buf *ByteBuffer, m map[string]int32, hasGenerics bool) } // readMapStringInt32 reads map[string]int32 using chunk protocol -func readMapStringInt32(buf *ByteBuffer, err *Error) map[string]int32 { +func readMapStringInt32(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]int32 { size := int(buf.ReadVarUint32(err)) +if maxMapSize > 0 && size > maxMapSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxMapSize)) + return nil +} result := make(map[string]int32, size) if size == 0 { return result @@ -271,7 +291,8 @@ func readMapStringInt32(buf *ByteBuffer, err *Error) map[string]int32 { buf.ReadUint8(err) } for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err) + k := readString(buf, err, maxStringBytes) + v := buf.ReadVarint32(err) result[k] = v size-- @@ -320,8 +341,13 @@ func writeMapStringInt(buf *ByteBuffer, m map[string]int, hasGenerics bool) { } // readMapStringInt reads map[string]int using chunk protocol -func readMapStringInt(buf *ByteBuffer, err *Error) map[string]int { +func readMapStringInt(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]int { size := int(buf.ReadVarUint32(err)) +if maxMapSize > 0 && size > maxMapSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxMapSize)) + return nil +} result := make(map[string]int, size) if size == 0 { return result @@ -345,7 +371,8 @@ func readMapStringInt(buf *ByteBuffer, err *Error) map[string]int { buf.ReadUint8(err) } for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err) + k := readString(buf, err, maxStringBytes) + v := buf.ReadVarint64(err) result[k] = int(v) size-- @@ -394,8 +421,13 @@ func writeMapStringFloat64(buf *ByteBuffer, m map[string]float64, hasGenerics bo } // readMapStringFloat64 reads map[string]float64 using chunk protocol -func readMapStringFloat64(buf *ByteBuffer, err *Error) map[string]float64 { +func readMapStringFloat64(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]float64 { size := int(buf.ReadVarUint32(err)) +if maxMapSize > 0 && size > maxMapSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxMapSize)) + return nil +} result := make(map[string]float64, size) if size == 0 { return result @@ -419,7 +451,8 @@ func readMapStringFloat64(buf *ByteBuffer, err *Error) map[string]float64 { buf.ReadUint8(err) } for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err) + k := readString(buf, err, maxStringBytes) + v := buf.ReadFloat64(err) result[k] = v size-- @@ -468,8 +501,13 @@ func writeMapStringBool(buf *ByteBuffer, m map[string]bool, hasGenerics bool) { } // readMapStringBool reads map[string]bool using chunk protocol -func readMapStringBool(buf *ByteBuffer, err *Error) map[string]bool { +func readMapStringBool(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]bool { size := int(buf.ReadVarUint32(err)) + if maxMapSize > 0 && size > maxMapSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxMapSize)) + return nil + } result := make(map[string]bool, size) if size == 0 { return result @@ -498,7 +536,8 @@ func readMapStringBool(buf *ByteBuffer, err *Error) map[string]bool { } for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err) + k := readString(buf, err, maxStringBytes) + v := buf.ReadBool(err) result[k] = v size-- @@ -549,6 +588,11 @@ func writeMapInt32Int32(buf *ByteBuffer, m map[int32]int32, hasGenerics bool) { // readMapInt32Int32 reads map[int32]int32 using chunk protocol func readMapInt32Int32(buf *ByteBuffer, err *Error) map[int32]int32 { size := int(buf.ReadVarUint32(err)) +if maxMapSize > 0 && size > maxMapSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxMapSize)) + return nil +} result := make(map[int32]int32, size) if size == 0 { return result @@ -623,6 +667,11 @@ func writeMapInt64Int64(buf *ByteBuffer, m map[int64]int64, hasGenerics bool) { // readMapInt64Int64 reads map[int64]int64 using chunk protocol func readMapInt64Int64(buf *ByteBuffer, err *Error) map[int64]int64 { size := int(buf.ReadVarUint32(err)) +if maxMapSize > 0 && size > maxMapSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxMapSize)) + return nil +} result := make(map[int64]int64, size) if size == 0 { return result @@ -697,6 +746,11 @@ func writeMapIntInt(buf *ByteBuffer, m map[int]int, hasGenerics bool) { // readMapIntInt reads map[int]int using chunk protocol func readMapIntInt(buf *ByteBuffer, err *Error) map[int]int { size := int(buf.ReadVarUint32(err)) +if maxMapSize > 0 && size > maxMapSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxMapSize)) + return nil +} result := make(map[int]int, size) if size == 0 { return result @@ -752,7 +806,7 @@ func (s stringStringMapSerializer) ReadData(ctx *ReadContext, value reflect.Valu value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapStringString(ctx.buffer, ctx.Err()) + result := readMapStringString(ctx.buffer, ctx.Err(), ctx.maxStringBytes, ctx.maxMapSize) value.Set(reflect.ValueOf(result)) } @@ -787,7 +841,7 @@ func (s stringInt64MapSerializer) ReadData(ctx *ReadContext, value reflect.Value value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapStringInt64(ctx.buffer, ctx.Err()) + result := readMapStringInt64(ctx.buffer, ctx.Err(), ctx.maxStringBytes, ctx.maxMapSize) value.Set(reflect.ValueOf(result)) } @@ -822,7 +876,7 @@ func (s stringIntMapSerializer) ReadData(ctx *ReadContext, value reflect.Value) value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapStringInt(ctx.buffer, ctx.Err()) + result := readMapStringInt(ctx.buffer, ctx.Err(), ctx.maxStringBytes, ctx.maxMapSize) value.Set(reflect.ValueOf(result)) } @@ -857,7 +911,7 @@ func (s stringFloat64MapSerializer) ReadData(ctx *ReadContext, value reflect.Val value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapStringFloat64(ctx.buffer, ctx.Err()) + result := readMapStringFloat64(ctx.buffer, ctx.Err(), ctx.maxStringBytes, ctx.maxMapSize) value.Set(reflect.ValueOf(result)) } @@ -892,7 +946,7 @@ func (s stringBoolMapSerializer) ReadData(ctx *ReadContext, value reflect.Value) value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapStringBool(ctx.buffer, ctx.Err()) + result := readMapStringBool(ctx.buffer, ctx.Err(), ctx.maxStringBytes, ctx.maxMapSize) value.Set(reflect.ValueOf(result)) } diff --git a/go/fory/reader.go b/go/fory/reader.go index e7a1df1710..c455fffa0d 100644 --- a/go/fory/reader.go +++ b/go/fory/reader.go @@ -43,6 +43,9 @@ type ReadContext struct { err Error // Accumulated error state for deferred checking lastTypePtr uintptr lastTypeInfo *TypeInfo + maxStringBytes int + maxCollectionSize int + maxMapSize int } // IsXlang returns whether cross-language serialization mode is enabled @@ -224,7 +227,7 @@ func (c *ReadContext) readFast(ptr unsafe.Pointer, ct DispatchId) { case PrimitiveFloat16DispatchId: *(*uint16)(ptr) = c.buffer.ReadUint16(err) case StringDispatchId: - *(*string)(ptr) = readString(c.buffer, err) + *(*string)(ptr) = readString(c.buffer, err, c.maxStringBytes) } } @@ -251,7 +254,7 @@ func (c *ReadContext) ReadLength() int { // ReadString reads a string value (caller handles nullable/type meta) func (c *ReadContext) ReadString() string { - return readString(c.buffer, c.Err()) + return readString(c.buffer, c.Err(), c.maxStringBytes) } // ReadBoolSlice reads []bool with ref/type info @@ -914,3 +917,26 @@ func (c *ReadContext) ReadArrayValue(target reflect.Value, refMode RefMode, read c.RefResolver().SetReadObject(refID, target) } } + +func (ctx *ReadContext) checkStringBytes(n int) { + if ctx.maxStringBytes > 0 && n > ctx.maxStringBytes { + ctx.SetError(DeserializationErrorf( + "fory: string byte length %d exceeds limit %d", n, ctx.maxStringBytes)) + } +} + +func (ctx *ReadContext) checkCollectionSize(n int) { + if ctx.maxCollectionSize > 0 && n > ctx.maxCollectionSize { + ctx.SetError(DeserializationErrorf( + "fory: collection size %d exceeds limit %d", n, ctx.maxCollectionSize)) + } +} + +func (ctx *ReadContext) checkMapSize(n int) { + if ctx.maxMapSize > 0 && n > ctx.maxMapSize { + ctx.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", n, ctx.maxMapSize)) + } +} + + diff --git a/go/fory/slice.go b/go/fory/slice.go index bd3a9aa7ee..2e8c1efd1a 100644 --- a/go/fory/slice.go +++ b/go/fory/slice.go @@ -265,6 +265,8 @@ func (s *sliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() length := int(buf.ReadVarUint32(ctxErr)) + ctx.checkCollectionSize(length) + if ctx.HasError() { return } isArrayType := value.Type().Kind() == reflect.Array if length == 0 { diff --git a/go/fory/slice_primitive.go b/go/fory/slice_primitive.go index e4daf990be..fce61d7d25 100644 --- a/go/fory/slice_primitive.go +++ b/go/fory/slice_primitive.go @@ -643,6 +643,8 @@ func (s stringSliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() length := int(buf.ReadVarUint32(ctxErr)) + ctx.checkCollectionSize(length) + if ctx.HasError() { return } ptr := (*[]string)(value.Addr().UnsafePointer()) if length == 0 { *ptr = make([]string, 0) @@ -670,7 +672,7 @@ func (s stringSliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { continue // null string, leave as zero value } } - result[i] = readString(buf, ctxErr) + result[i] = readString(buf, ctxErr, ctx.maxStringBytes) } *ptr = result } diff --git a/go/fory/string.go b/go/fory/string.go index 10586a27cd..94275cb875 100644 --- a/go/fory/string.go +++ b/go/fory/string.go @@ -49,11 +49,17 @@ func writeString(buf *ByteBuffer, value string) { } // readString reads a string from buffer using xlang encoding -func readString(buf *ByteBuffer, err *Error) string { +func readString(buf *ByteBuffer, err *Error, maxBytes int) string { header := buf.ReadVaruint36Small(err) size := header >> 2 // Extract byte count encoding := header & 0b11 // Extract encoding type + if maxBytes > 0 && int(size) > maxBytes { + err.SetError(DeserializationErrorf( + "fory: string byte length %d exceeds limit %d", int(size), maxBytes)) + return "" + } + switch encoding { case encodingLatin1: return readLatin1(buf, int(size), err) @@ -132,7 +138,7 @@ func (s stringSerializer) Write(ctx *WriteContext, refMode RefMode, writeType bo func (s stringSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() - str := readString(ctx.buffer, err) + str := readString(ctx.buffer, err, ctx.maxStringBytes) if ctx.HasError() { return } @@ -202,7 +208,7 @@ func (s ptrToStringSerializer) Read(ctx *ReadContext, refMode RefMode, readType func (s ptrToStringSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() - str := readString(ctx.buffer, err) + str := readString(ctx.buffer, err, ctx.maxStringBytes) if ctx.HasError() { return } From d8a55fc5c9d0cbb3def520343a31247809b8ce76 Mon Sep 17 00:00:00 2001 From: Zakir Date: Sun, 1 Mar 2026 11:32:50 +0530 Subject: [PATCH 3/5] imp --- go/fory/fory.go | 54 +++---- go/fory/limits_test.go | 287 +++++++++++-------------------------- go/fory/map.go | 2 +- go/fory/map_primitive.go | 149 +++++++++---------- go/fory/reader.go | 66 ++++----- go/fory/slice.go | 6 +- go/fory/slice_dyn.go | 4 + go/fory/slice_primitive.go | 12 +- go/fory/string.go | 12 +- 9 files changed, 228 insertions(+), 364 deletions(-) diff --git a/go/fory/fory.go b/go/fory/fory.go index efb63ae783..43755603ca 100644 --- a/go/fory/fory.go +++ b/go/fory/fory.go @@ -50,21 +50,22 @@ const ( // Config holds configuration options for Fory instances type Config struct { - TrackRef bool - MaxDepth int - IsXlang bool - Compatible bool // Schema evolution compatibility mode - MaxStringBytes int - MaxCollectionSize int - MaxMapSize int + TrackRef bool + MaxDepth int + IsXlang bool + Compatible bool // Schema evolution compatibility mode + MaxBinarySize int // Maximum byte length for a single deserialized binary payload (0 = no limit) + MaxCollectionSize int // Maximum element count for a single deserialized collection or map (0 = no limit) } // defaultConfig returns the default configuration func defaultConfig() Config { return Config{ - TrackRef: false, // Match Java's default: reference tracking disabled - MaxDepth: 20, - IsXlang: false, + TrackRef: false, + MaxDepth: 20, + IsXlang: false, + MaxBinarySize: 64 * 1024 * 1024, // 64 MB + MaxCollectionSize: 1_000_000, } } @@ -104,31 +105,20 @@ func WithCompatible(enabled bool) Option { } } -// WithMaxStringBytes sets the maximum allowed byte length for a single -// deserialized string. 0 (default) means no limit. -func WithMaxStringBytes(n int) Option { - return func(f *Fory) { - f.config.MaxStringBytes = n - } +// WithMaxBinarySize sets the maximum byte length for a single deserialized binary payload. +func WithMaxBinarySize(n int) Option { + return func(f *Fory) { + f.config.MaxBinarySize = n + } } -// WithMaxCollectionSize sets the maximum allowed element count for a single -// deserialized slice or list. 0 (default) means no limit. +// WithMaxCollectionSize sets the maximum element count for a single deserialized collection or map. func WithMaxCollectionSize(n int) Option { - return func(f *Fory) { - f.config.MaxCollectionSize = n - } -} - -// WithMaxMapSize sets the maximum allowed entry count for a single -// deserialized map. 0 (default) means no limit. -func WithMaxMapSize(n int) Option { - return func(f *Fory) { - f.config.MaxMapSize = n - } + return func(f *Fory) { + f.config.MaxCollectionSize = n + } } - // ============================================================================ // Fory - Main serialization instance // ============================================================================ @@ -184,10 +174,8 @@ func New(opts ...Option) *Fory { f.readCtx.refResolver = f.refResolver f.readCtx.compatible = f.config.Compatible f.readCtx.xlang = f.config.IsXlang - f.readCtx.maxStringBytes = f.config.MaxStringBytes + f.readCtx.maxBinarySize = f.config.MaxBinarySize f.readCtx.maxCollectionSize = f.config.MaxCollectionSize - f.readCtx.maxMapSize = f.config.MaxMapSize - return f } diff --git a/go/fory/limits_test.go b/go/fory/limits_test.go index a4214431c3..1a705cce5d 100644 --- a/go/fory/limits_test.go +++ b/go/fory/limits_test.go @@ -18,221 +18,108 @@ package fory import ( - "fmt" "strings" "testing" - - "github.com/stretchr/testify/require" ) -// ============================================================================ -// MaxStringBytes -// ============================================================================ - -func TestMaxStringBytesBlocksOversizedString(t *testing.T) { - f := NewFory(WithXlang(true), WithMaxStringBytes(5)) - - long := strings.Repeat("a", 20) // 20 bytes > limit 5 - data, err := f.Marshal(long) - require.NoError(t, err) // write path has no limit - - var result string - err = f.Unmarshal(data, &result) - require.Error(t, err, "expected error: string exceeds MaxStringBytes") -} - -func TestMaxStringBytesAllowsExactLimit(t *testing.T) { - f := NewFory(WithXlang(true), WithMaxStringBytes(5)) - - s := "hello" // exactly 5 bytes — must NOT be rejected (> not >=) - data, err := f.Marshal(s) - require.NoError(t, err) - - var result string - require.NoError(t, f.Unmarshal(data, &result)) - require.Equal(t, s, result) -} - -func TestMaxStringBytesAllowsWithinLimit(t *testing.T) { - f := NewFory(WithXlang(true), WithMaxStringBytes(10)) - - s := "hi" // 2 bytes, well within limit - data, err := f.Marshal(s) - require.NoError(t, err) - - var result string - require.NoError(t, f.Unmarshal(data, &result)) - require.Equal(t, s, result) -} - -func TestMaxStringBytesZeroMeansNoLimit(t *testing.T) { - f := NewFory(WithXlang(true)) // default 0 = no limit - - long := strings.Repeat("x", 100_000) - data, err := f.Marshal(long) - require.NoError(t, err) - - var result string - require.NoError(t, f.Unmarshal(data, &result)) - require.Equal(t, long, result) -} - -// ============================================================================ -// MaxCollectionSize -// ============================================================================ - -func TestMaxCollectionSizeBlocksOversizedSlice(t *testing.T) { - f := NewFory(WithXlang(true), WithMaxCollectionSize(3)) - - s := []string{"a", "b", "c", "d", "e"} // 5 elements > limit 3 - data, err := f.Marshal(s) - require.NoError(t, err) - - var result []string - err = f.Unmarshal(data, &result) - require.Error(t, err, "expected error: slice exceeds MaxCollectionSize") -} - -func TestMaxCollectionSizeAllowsWithinLimit(t *testing.T) { - f := NewFory(WithXlang(true), WithMaxCollectionSize(5)) - - s := []string{"a", "b", "c"} // 3 elements, within limit 5 - data, err := f.Marshal(s) - require.NoError(t, err) - - var result []string - require.NoError(t, f.Unmarshal(data, &result)) - require.Equal(t, s, result) -} - -func TestMaxCollectionSizeAllowsExactLimit(t *testing.T) { - f := NewFory(WithXlang(true), WithMaxCollectionSize(3)) - - s := []string{"a", "b", "c"} // exactly 3 — must NOT be rejected - data, err := f.Marshal(s) - require.NoError(t, err) - - var result []string - require.NoError(t, f.Unmarshal(data, &result)) - require.Equal(t, s, result) -} - -func TestMaxCollectionSizeZeroMeansNoLimit(t *testing.T) { - f := NewFory(WithXlang(true)) // default 0 = no limit - - s := make([]int32, 10_000) - data, err := f.Marshal(s) - require.NoError(t, err) - - var result []int32 - require.NoError(t, f.Unmarshal(data, &result)) - require.Equal(t, len(s), len(result)) -} - -// ============================================================================ -// MaxMapSize -// ============================================================================ - -func TestMaxMapSizeBlocksOversizedMap(t *testing.T) { - f := NewFory(WithXlang(true), WithMaxMapSize(2)) - - m := map[string]string{"k1": "v1", "k2": "v2", "k3": "v3"} // 3 entries > limit 2 - data, err := f.Marshal(m) - require.NoError(t, err) - - var result map[string]string - err = f.Unmarshal(data, &result) - require.Error(t, err, "expected error: map exceeds MaxMapSize") -} - -func TestMaxMapSizeAllowsWithinLimit(t *testing.T) { - f := NewFory(WithXlang(true), WithMaxMapSize(5)) - - m := map[string]string{"k1": "v1", "k2": "v2"} - data, err := f.Marshal(m) - require.NoError(t, err) +// TestSizeGuardrails_SliceExceedsLimit verifies that deserializing a slice +// whose element count exceeds MaxCollectionSize returns an error. +func TestSizeGuardrails_SliceExceedsLimit(t *testing.T) { + // Serialize a string slice with 5 elements using no limit + f1 := New(WithXlang(true), WithMaxCollectionSize(0)) + data := []string{"a", "b", "c", "d", "e"} + bytes, err := f1.Marshal(data) + if err != nil { + t.Fatalf("serialize failed: %v", err) + } - var result map[string]string - require.NoError(t, f.Unmarshal(data, &result)) - require.Equal(t, m, result) + // Deserialize with a limit of 3 — should fail + f2 := New(WithXlang(true), WithMaxCollectionSize(3)) + var result any + err2 := f2.Unmarshal(bytes, &result) + if err2 == nil { + t.Fatal("expected error when collection size exceeds limit, got nil") + } + if !strings.Contains(err2.Error(), "exceeds limit") { + t.Fatalf("expected 'exceeds limit' error, got: %v", err2) + } } -func TestMaxMapSizeAllowsExactLimit(t *testing.T) { - f := NewFory(WithXlang(true), WithMaxMapSize(2)) - - m := map[string]string{"k1": "v1", "k2": "v2"} // exactly 2 — must NOT be rejected - data, err := f.Marshal(m) - require.NoError(t, err) - - var result map[string]string - require.NoError(t, f.Unmarshal(data, &result)) - require.Equal(t, m, result) +// TestSizeGuardrails_SliceWithinLimit verifies that a slice within limits +// deserializes successfully. +func TestSizeGuardrails_SliceWithinLimit(t *testing.T) { + f := New(WithXlang(true), WithMaxCollectionSize(100)) + data := []int32{1, 2, 3} + bytes, err := f.Marshal(data) + if err != nil { + t.Fatalf("serialize failed: %v", err) + } + var result any + err = f.Unmarshal(bytes, &result) + if err != nil { + t.Fatalf("deserialize should succeed within limit: %v", err) + } } -func TestMaxMapSizeZeroMeansNoLimit(t *testing.T) { - f := NewFory(WithXlang(true)) // default 0 = no limit - - m := make(map[string]string, 1000) - for i := 0; i < 1000; i++ { - m[fmt.Sprintf("k%d", i)] = "v" +// TestSizeGuardrails_MapExceedsLimit verifies that deserializing a map +// whose entry count exceeds MaxCollectionSize returns an error. +func TestSizeGuardrails_MapExceedsLimit(t *testing.T) { + f1 := New(WithXlang(true), WithMaxCollectionSize(0)) + m := map[string]string{"a": "1", "b": "2", "c": "3", "d": "4", "e": "5"} + bytes, err := f1.Marshal(m) + if err != nil { + t.Fatalf("serialize failed: %v", err) } - data, err := f.Marshal(m) - require.NoError(t, err) - var result map[string]string - require.NoError(t, f.Unmarshal(data, &result)) - require.Equal(t, 1000, len(result)) + f2 := New(WithXlang(true), WithMaxCollectionSize(2)) + var result any + err2 := f2.Unmarshal(bytes, &result) + if err2 == nil { + t.Fatal("expected error when map size exceeds limit, got nil") + } + if !strings.Contains(err2.Error(), "exceeds limit") { + t.Fatalf("expected 'exceeds limit' error, got: %v", err2) + } } -// ============================================================================ -// Combined limits -// ============================================================================ - -func TestCombinedLimitsStringInsideSlice(t *testing.T) { - // Slice size is within limit, but one element string is too long - f := NewFory(WithXlang(true), WithMaxCollectionSize(10), WithMaxStringBytes(3)) - - s := []string{"ab", "cd", "this-is-too-long"} // third element 16 bytes > limit 3 - data, err := f.Marshal(s) - require.NoError(t, err) - - var result []string - err = f.Unmarshal(data, &result) - require.Error(t, err) +// TestSizeGuardrails_MapWithinLimit verifies that a map within limits +// deserializes successfully. +func TestSizeGuardrails_MapWithinLimit(t *testing.T) { + f := New(WithXlang(true), WithMaxCollectionSize(100)) + m := map[string]string{"a": "1", "b": "2"} + bytes, err := f.Marshal(m) + if err != nil { + t.Fatalf("serialize failed: %v", err) + } + var result any + err = f.Unmarshal(bytes, &result) + if err != nil { + t.Fatalf("deserialize should succeed within limit: %v", err) + } } -func TestCombinedLimitsCollectionFiresBeforeString(t *testing.T) { - // Collection limit fires before any string element is read - f := NewFory(WithXlang(true), WithMaxCollectionSize(2), WithMaxStringBytes(1000)) - - s := []string{"a", "b", "c", "d"} // 4 elements > collection limit 2 - data, err := f.Marshal(s) - require.NoError(t, err) - - var result []string - err = f.Unmarshal(data, &result) - require.Error(t, err) +// TestSizeGuardrails_DefaultConfig verifies that default limits are set. +func TestSizeGuardrails_DefaultConfig(t *testing.T) { + f := New() + if f.config.MaxBinarySize != 64*1024*1024 { + t.Fatalf("expected default MaxBinarySize=64MB, got %d", f.config.MaxBinarySize) + } + if f.config.MaxCollectionSize != 1_000_000 { + t.Fatalf("expected default MaxCollectionSize=1000000, got %d", f.config.MaxCollectionSize) + } } -func TestCombinedLimitsAllWithinBounds(t *testing.T) { - // All limits set, all values within bounds — must succeed end-to-end - f := NewFory(WithXlang(true), - WithMaxStringBytes(20), - WithMaxCollectionSize(10), - WithMaxMapSize(10), - ) - - s := []string{"hello", "world"} - data, err := f.Marshal(s) - require.NoError(t, err) - var sliceResult []string - require.NoError(t, f.Unmarshal(data, &sliceResult)) - require.Equal(t, s, sliceResult) - - m := map[string]string{"k1": "v1", "k2": "v2"} - data, err = f.Marshal(m) - require.NoError(t, err) - var mapResult map[string]string - require.NoError(t, f.Unmarshal(data, &mapResult)) - require.Equal(t, m, mapResult) +// TestSizeGuardrails_NoLimitWhenZero verifies that 0 means unlimited. +func TestSizeGuardrails_NoLimitWhenZero(t *testing.T) { + f := New(WithXlang(true), WithMaxCollectionSize(0)) + data := []int32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + bytes, err := f.Marshal(data) + if err != nil { + t.Fatalf("serialize failed: %v", err) + } + var result any + err = f.Unmarshal(bytes, &result) + if err != nil { + t.Fatalf("deserialize with no limit should succeed: %v", err) + } } diff --git a/go/fory/map.go b/go/fory/map.go index 91dc0bcd24..cf299d5bee 100644 --- a/go/fory/map.go +++ b/go/fory/map.go @@ -306,7 +306,7 @@ func (s mapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { refResolver.Reference(value) size := int(buf.ReadVarUint32(ctxErr)) - ctx.checkMapSize(size) + ctx.checkCollectionSize(size) if size == 0 || ctx.HasError() { return } diff --git a/go/fory/map_primitive.go b/go/fory/map_primitive.go index 0cfbbd076f..b3c301fe54 100644 --- a/go/fory/map_primitive.go +++ b/go/fory/map_primitive.go @@ -69,13 +69,13 @@ func writeMapStringString(buf *ByteBuffer, m map[string]string, hasGenerics bool } // readMapStringString reads map[string]string using chunk protocol -func readMapStringString(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]string { +func readMapStringString(buf *ByteBuffer, err *Error, maxCollectionSize int) map[string]string { size := int(buf.ReadVarUint32(err)) -if maxMapSize > 0 && size > maxMapSize { - err.SetError(DeserializationErrorf( - "fory: map size %d exceeds limit %d", size, maxMapSize)) - return nil -} + if maxCollectionSize > 0 && size > maxCollectionSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxCollectionSize)) + return nil + } result := make(map[string]string, size) if size == 0 { return result @@ -99,8 +99,7 @@ if maxMapSize > 0 && size > maxMapSize { if !valueDeclared { buf.ReadUint8(err) // skip value type } - v := readString(buf, err, maxStringBytes) - + v := readString(buf, err) result[""] = v // empty string as null key size-- continue @@ -110,8 +109,7 @@ if maxMapSize > 0 && size > maxMapSize { if !keyDeclared { buf.ReadUint8(err) // skip key type } - k := readString(buf, err, maxStringBytes) - + k := readString(buf, err) result[k] = "" // empty string as null value size-- continue @@ -130,10 +128,8 @@ if maxMapSize > 0 && size > maxMapSize { // ReadData chunk entries for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err, maxStringBytes) - - v := readString(buf, err, maxStringBytes) - + k := readString(buf, err) + v := readString(buf, err) result[k] = v size-- } @@ -181,13 +177,13 @@ func writeMapStringInt64(buf *ByteBuffer, m map[string]int64, hasGenerics bool) } // readMapStringInt64 reads map[string]int64 using chunk protocol -func readMapStringInt64(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]int64 { +func readMapStringInt64(buf *ByteBuffer, err *Error, maxCollectionSize int) map[string]int64 { size := int(buf.ReadVarUint32(err)) -if maxMapSize > 0 && size > maxMapSize { - err.SetError(DeserializationErrorf( - "fory: map size %d exceeds limit %d", size, maxMapSize)) - return nil -} + if maxCollectionSize > 0 && size > maxCollectionSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxCollectionSize)) + return nil + } result := make(map[string]int64, size) if size == 0 { return result @@ -211,8 +207,7 @@ if maxMapSize > 0 && size > maxMapSize { buf.ReadUint8(err) } for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err, maxStringBytes) - + k := readString(buf, err) v := buf.ReadVarint64(err) result[k] = v size-- @@ -261,13 +256,13 @@ func writeMapStringInt32(buf *ByteBuffer, m map[string]int32, hasGenerics bool) } // readMapStringInt32 reads map[string]int32 using chunk protocol -func readMapStringInt32(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]int32 { +func readMapStringInt32(buf *ByteBuffer, err *Error, maxCollectionSize int) map[string]int32 { size := int(buf.ReadVarUint32(err)) -if maxMapSize > 0 && size > maxMapSize { - err.SetError(DeserializationErrorf( - "fory: map size %d exceeds limit %d", size, maxMapSize)) - return nil -} + if maxCollectionSize > 0 && size > maxCollectionSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxCollectionSize)) + return nil + } result := make(map[string]int32, size) if size == 0 { return result @@ -291,8 +286,7 @@ if maxMapSize > 0 && size > maxMapSize { buf.ReadUint8(err) } for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err, maxStringBytes) - + k := readString(buf, err) v := buf.ReadVarint32(err) result[k] = v size-- @@ -341,13 +335,13 @@ func writeMapStringInt(buf *ByteBuffer, m map[string]int, hasGenerics bool) { } // readMapStringInt reads map[string]int using chunk protocol -func readMapStringInt(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]int { +func readMapStringInt(buf *ByteBuffer, err *Error, maxCollectionSize int) map[string]int { size := int(buf.ReadVarUint32(err)) -if maxMapSize > 0 && size > maxMapSize { - err.SetError(DeserializationErrorf( - "fory: map size %d exceeds limit %d", size, maxMapSize)) - return nil -} + if maxCollectionSize > 0 && size > maxCollectionSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxCollectionSize)) + return nil + } result := make(map[string]int, size) if size == 0 { return result @@ -371,8 +365,7 @@ if maxMapSize > 0 && size > maxMapSize { buf.ReadUint8(err) } for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err, maxStringBytes) - + k := readString(buf, err) v := buf.ReadVarint64(err) result[k] = int(v) size-- @@ -421,13 +414,13 @@ func writeMapStringFloat64(buf *ByteBuffer, m map[string]float64, hasGenerics bo } // readMapStringFloat64 reads map[string]float64 using chunk protocol -func readMapStringFloat64(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]float64 { +func readMapStringFloat64(buf *ByteBuffer, err *Error, maxCollectionSize int) map[string]float64 { size := int(buf.ReadVarUint32(err)) -if maxMapSize > 0 && size > maxMapSize { - err.SetError(DeserializationErrorf( - "fory: map size %d exceeds limit %d", size, maxMapSize)) - return nil -} + if maxCollectionSize > 0 && size > maxCollectionSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxCollectionSize)) + return nil + } result := make(map[string]float64, size) if size == 0 { return result @@ -451,8 +444,7 @@ if maxMapSize > 0 && size > maxMapSize { buf.ReadUint8(err) } for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err, maxStringBytes) - + k := readString(buf, err) v := buf.ReadFloat64(err) result[k] = v size-- @@ -501,12 +493,12 @@ func writeMapStringBool(buf *ByteBuffer, m map[string]bool, hasGenerics bool) { } // readMapStringBool reads map[string]bool using chunk protocol -func readMapStringBool(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize int) map[string]bool { +func readMapStringBool(buf *ByteBuffer, err *Error, maxCollectionSize int) map[string]bool { size := int(buf.ReadVarUint32(err)) - if maxMapSize > 0 && size > maxMapSize { - err.SetError(DeserializationErrorf( - "fory: map size %d exceeds limit %d", size, maxMapSize)) - return nil + if maxCollectionSize > 0 && size > maxCollectionSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxCollectionSize)) + return nil } result := make(map[string]bool, size) if size == 0 { @@ -536,8 +528,7 @@ func readMapStringBool(buf *ByteBuffer, err *Error, maxStringBytes, maxMapSize i } for i := 0; i < chunkSize && size > 0; i++ { - k := readString(buf, err, maxStringBytes) - + k := readString(buf, err) v := buf.ReadBool(err) result[k] = v size-- @@ -586,13 +577,13 @@ func writeMapInt32Int32(buf *ByteBuffer, m map[int32]int32, hasGenerics bool) { } // readMapInt32Int32 reads map[int32]int32 using chunk protocol -func readMapInt32Int32(buf *ByteBuffer, err *Error) map[int32]int32 { +func readMapInt32Int32(buf *ByteBuffer, err *Error, maxCollectionSize int) map[int32]int32 { size := int(buf.ReadVarUint32(err)) -if maxMapSize > 0 && size > maxMapSize { - err.SetError(DeserializationErrorf( - "fory: map size %d exceeds limit %d", size, maxMapSize)) - return nil -} + if maxCollectionSize > 0 && size > maxCollectionSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxCollectionSize)) + return nil + } result := make(map[int32]int32, size) if size == 0 { return result @@ -665,13 +656,13 @@ func writeMapInt64Int64(buf *ByteBuffer, m map[int64]int64, hasGenerics bool) { } // readMapInt64Int64 reads map[int64]int64 using chunk protocol -func readMapInt64Int64(buf *ByteBuffer, err *Error) map[int64]int64 { +func readMapInt64Int64(buf *ByteBuffer, err *Error, maxCollectionSize int) map[int64]int64 { size := int(buf.ReadVarUint32(err)) -if maxMapSize > 0 && size > maxMapSize { - err.SetError(DeserializationErrorf( - "fory: map size %d exceeds limit %d", size, maxMapSize)) - return nil -} + if maxCollectionSize > 0 && size > maxCollectionSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxCollectionSize)) + return nil + } result := make(map[int64]int64, size) if size == 0 { return result @@ -744,13 +735,13 @@ func writeMapIntInt(buf *ByteBuffer, m map[int]int, hasGenerics bool) { } // readMapIntInt reads map[int]int using chunk protocol -func readMapIntInt(buf *ByteBuffer, err *Error) map[int]int { +func readMapIntInt(buf *ByteBuffer, err *Error, maxCollectionSize int) map[int]int { size := int(buf.ReadVarUint32(err)) -if maxMapSize > 0 && size > maxMapSize { - err.SetError(DeserializationErrorf( - "fory: map size %d exceeds limit %d", size, maxMapSize)) - return nil -} + if maxCollectionSize > 0 && size > maxCollectionSize { + err.SetError(DeserializationErrorf( + "fory: map size %d exceeds limit %d", size, maxCollectionSize)) + return nil + } result := make(map[int]int, size) if size == 0 { return result @@ -806,7 +797,7 @@ func (s stringStringMapSerializer) ReadData(ctx *ReadContext, value reflect.Valu value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapStringString(ctx.buffer, ctx.Err(), ctx.maxStringBytes, ctx.maxMapSize) + result := readMapStringString(ctx.buffer, ctx.Err(), ctx.maxCollectionSize) value.Set(reflect.ValueOf(result)) } @@ -841,7 +832,7 @@ func (s stringInt64MapSerializer) ReadData(ctx *ReadContext, value reflect.Value value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapStringInt64(ctx.buffer, ctx.Err(), ctx.maxStringBytes, ctx.maxMapSize) + result := readMapStringInt64(ctx.buffer, ctx.Err(), ctx.maxCollectionSize) value.Set(reflect.ValueOf(result)) } @@ -876,7 +867,7 @@ func (s stringIntMapSerializer) ReadData(ctx *ReadContext, value reflect.Value) value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapStringInt(ctx.buffer, ctx.Err(), ctx.maxStringBytes, ctx.maxMapSize) + result := readMapStringInt(ctx.buffer, ctx.Err(), ctx.maxCollectionSize) value.Set(reflect.ValueOf(result)) } @@ -911,7 +902,7 @@ func (s stringFloat64MapSerializer) ReadData(ctx *ReadContext, value reflect.Val value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapStringFloat64(ctx.buffer, ctx.Err(), ctx.maxStringBytes, ctx.maxMapSize) + result := readMapStringFloat64(ctx.buffer, ctx.Err(), ctx.maxCollectionSize) value.Set(reflect.ValueOf(result)) } @@ -946,7 +937,7 @@ func (s stringBoolMapSerializer) ReadData(ctx *ReadContext, value reflect.Value) value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapStringBool(ctx.buffer, ctx.Err(), ctx.maxStringBytes, ctx.maxMapSize) + result := readMapStringBool(ctx.buffer, ctx.Err(), ctx.maxCollectionSize) value.Set(reflect.ValueOf(result)) } @@ -981,7 +972,7 @@ func (s int32Int32MapSerializer) ReadData(ctx *ReadContext, value reflect.Value) value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapInt32Int32(ctx.buffer, ctx.Err()) + result := readMapInt32Int32(ctx.buffer, ctx.Err(), ctx.maxCollectionSize) value.Set(reflect.ValueOf(result)) } @@ -1016,7 +1007,7 @@ func (s int64Int64MapSerializer) ReadData(ctx *ReadContext, value reflect.Value) value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapInt64Int64(ctx.buffer, ctx.Err()) + result := readMapInt64Int64(ctx.buffer, ctx.Err(), ctx.maxCollectionSize) value.Set(reflect.ValueOf(result)) } @@ -1051,7 +1042,7 @@ func (s intIntMapSerializer) ReadData(ctx *ReadContext, value reflect.Value) { value.Set(reflect.MakeMap(value.Type())) } ctx.RefResolver().Reference(value) - result := readMapIntInt(ctx.buffer, ctx.Err()) + result := readMapIntInt(ctx.buffer, ctx.Err(), ctx.maxCollectionSize) value.Set(reflect.ValueOf(result)) } diff --git a/go/fory/reader.go b/go/fory/reader.go index c455fffa0d..6b0c0cc1ca 100644 --- a/go/fory/reader.go +++ b/go/fory/reader.go @@ -43,9 +43,8 @@ type ReadContext struct { err Error // Accumulated error state for deferred checking lastTypePtr uintptr lastTypeInfo *TypeInfo - maxStringBytes int - maxCollectionSize int - maxMapSize int + maxBinarySize int // Maximum byte length for a single binary payload (0 = no limit) + maxCollectionSize int // Maximum element count for a single collection or map (0 = no limit) } // IsXlang returns whether cross-language serialization mode is enabled @@ -151,6 +150,22 @@ func (c *ReadContext) CheckError() error { return nil } +// checkCollectionSize validates that a collection/map element count does not exceed the configured limit. +func (c *ReadContext) checkCollectionSize(size int) { + if c.maxCollectionSize > 0 && size > c.maxCollectionSize { + c.SetError(DeserializationErrorf( + "fory: collection/map size %d exceeds limit %d", size, c.maxCollectionSize)) + } +} + +// checkBinarySize validates that a binary payload byte length does not exceed the configured limit. +func (c *ReadContext) checkBinarySize(size int) { + if c.maxBinarySize > 0 && size > c.maxBinarySize { + c.SetError(DeserializationErrorf( + "fory: binary size %d exceeds limit %d", size, c.maxBinarySize)) + } +} + // Inline primitive reads func (c *ReadContext) RawBool() bool { return c.buffer.ReadBool(c.Err()) } func (c *ReadContext) RawInt8() int8 { return int8(c.buffer.ReadByte(c.Err())) } @@ -227,7 +242,7 @@ func (c *ReadContext) readFast(ptr unsafe.Pointer, ct DispatchId) { case PrimitiveFloat16DispatchId: *(*uint16)(ptr) = c.buffer.ReadUint16(err) case StringDispatchId: - *(*string)(ptr) = readString(c.buffer, err, c.maxStringBytes) + *(*string)(ptr) = readString(c.buffer, err) } } @@ -254,7 +269,7 @@ func (c *ReadContext) ReadLength() int { // ReadString reads a string value (caller handles nullable/type meta) func (c *ReadContext) ReadString() string { - return readString(c.buffer, c.Err(), c.maxStringBytes) + return readString(c.buffer, c.Err()) } // ReadBoolSlice reads []bool with ref/type info @@ -465,7 +480,7 @@ func (c *ReadContext) ReadStringStringMap(refMode RefMode, readType bool) map[st if readType { _ = c.buffer.ReadUint8(err) } - return readMapStringString(c.buffer, err) + return readMapStringString(c.buffer, err, c.maxCollectionSize) } // ReadStringInt64Map reads map[string]int64 with optional ref/type info @@ -479,7 +494,7 @@ func (c *ReadContext) ReadStringInt64Map(refMode RefMode, readType bool) map[str if readType { _ = c.buffer.ReadUint8(err) } - return readMapStringInt64(c.buffer, err) + return readMapStringInt64(c.buffer, err, c.maxCollectionSize) } // ReadStringInt32Map reads map[string]int32 with optional ref/type info @@ -493,7 +508,7 @@ func (c *ReadContext) ReadStringInt32Map(refMode RefMode, readType bool) map[str if readType { _ = c.buffer.ReadUint8(err) } - return readMapStringInt32(c.buffer, err) + return readMapStringInt32(c.buffer, err, c.maxCollectionSize) } // ReadStringIntMap reads map[string]int with optional ref/type info @@ -507,7 +522,7 @@ func (c *ReadContext) ReadStringIntMap(refMode RefMode, readType bool) map[strin if readType { _ = c.buffer.ReadUint8(err) } - return readMapStringInt(c.buffer, err) + return readMapStringInt(c.buffer, err, c.maxCollectionSize) } // ReadStringFloat64Map reads map[string]float64 with optional ref/type info @@ -521,7 +536,7 @@ func (c *ReadContext) ReadStringFloat64Map(refMode RefMode, readType bool) map[s if readType { _ = c.buffer.ReadUint8(err) } - return readMapStringFloat64(c.buffer, err) + return readMapStringFloat64(c.buffer, err, c.maxCollectionSize) } // ReadStringBoolMap reads map[string]bool with optional ref/type info @@ -535,7 +550,7 @@ func (c *ReadContext) ReadStringBoolMap(refMode RefMode, readType bool) map[stri if readType { _ = c.buffer.ReadUint8(err) } - return readMapStringBool(c.buffer, err) + return readMapStringBool(c.buffer, err, c.maxCollectionSize) } // ReadInt32Int32Map reads map[int32]int32 with optional ref/type info @@ -549,7 +564,7 @@ func (c *ReadContext) ReadInt32Int32Map(refMode RefMode, readType bool) map[int3 if readType { _ = c.buffer.ReadUint8(err) } - return readMapInt32Int32(c.buffer, err) + return readMapInt32Int32(c.buffer, err, c.maxCollectionSize) } // ReadInt64Int64Map reads map[int64]int64 with optional ref/type info @@ -563,7 +578,7 @@ func (c *ReadContext) ReadInt64Int64Map(refMode RefMode, readType bool) map[int6 if readType { _ = c.buffer.ReadUint8(err) } - return readMapInt64Int64(c.buffer, err) + return readMapInt64Int64(c.buffer, err, c.maxCollectionSize) } // ReadIntIntMap reads map[int]int with optional ref/type info @@ -577,7 +592,7 @@ func (c *ReadContext) ReadIntIntMap(refMode RefMode, readType bool) map[int]int if readType { _ = c.buffer.ReadUint8(err) } - return readMapIntInt(c.buffer, err) + return readMapIntInt(c.buffer, err, c.maxCollectionSize) } // ReadBufferObject reads a buffer object @@ -917,26 +932,3 @@ func (c *ReadContext) ReadArrayValue(target reflect.Value, refMode RefMode, read c.RefResolver().SetReadObject(refID, target) } } - -func (ctx *ReadContext) checkStringBytes(n int) { - if ctx.maxStringBytes > 0 && n > ctx.maxStringBytes { - ctx.SetError(DeserializationErrorf( - "fory: string byte length %d exceeds limit %d", n, ctx.maxStringBytes)) - } -} - -func (ctx *ReadContext) checkCollectionSize(n int) { - if ctx.maxCollectionSize > 0 && n > ctx.maxCollectionSize { - ctx.SetError(DeserializationErrorf( - "fory: collection size %d exceeds limit %d", n, ctx.maxCollectionSize)) - } -} - -func (ctx *ReadContext) checkMapSize(n int) { - if ctx.maxMapSize > 0 && n > ctx.maxMapSize { - ctx.SetError(DeserializationErrorf( - "fory: map size %d exceeds limit %d", n, ctx.maxMapSize)) - } -} - - diff --git a/go/fory/slice.go b/go/fory/slice.go index 2e8c1efd1a..b3960a5c4c 100644 --- a/go/fory/slice.go +++ b/go/fory/slice.go @@ -265,8 +265,10 @@ func (s *sliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() length := int(buf.ReadVarUint32(ctxErr)) - ctx.checkCollectionSize(length) - if ctx.HasError() { return } + ctx.checkCollectionSize(length) + if ctx.HasError() { + return + } isArrayType := value.Type().Kind() == reflect.Array if length == 0 { diff --git a/go/fory/slice_dyn.go b/go/fory/slice_dyn.go index 3393d4b22b..efdae2ce4a 100644 --- a/go/fory/slice_dyn.go +++ b/go/fory/slice_dyn.go @@ -262,6 +262,10 @@ func (s sliceDynSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() length := int(buf.ReadVarUint32(ctxErr)) + ctx.checkCollectionSize(length) + if ctx.HasError() { + return + } sliceType := value.Type() value.Set(reflect.MakeSlice(sliceType, length, length)) if length == 0 { diff --git a/go/fory/slice_primitive.go b/go/fory/slice_primitive.go index fce61d7d25..ce0c924fe6 100644 --- a/go/fory/slice_primitive.go +++ b/go/fory/slice_primitive.go @@ -75,6 +75,10 @@ func (s byteSliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() length := buf.ReadLength(ctxErr) + ctx.checkCollectionSize(length) + if ctx.HasError() { + return + } ptr := (*[]byte)(value.Addr().UnsafePointer()) if length == 0 { *ptr = make([]byte, 0) @@ -643,8 +647,10 @@ func (s stringSliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { buf := ctx.Buffer() ctxErr := ctx.Err() length := int(buf.ReadVarUint32(ctxErr)) - ctx.checkCollectionSize(length) - if ctx.HasError() { return } + ctx.checkCollectionSize(length) + if ctx.HasError() { + return + } ptr := (*[]string)(value.Addr().UnsafePointer()) if length == 0 { *ptr = make([]string, 0) @@ -672,7 +678,7 @@ func (s stringSliceSerializer) ReadData(ctx *ReadContext, value reflect.Value) { continue // null string, leave as zero value } } - result[i] = readString(buf, ctxErr, ctx.maxStringBytes) + result[i] = readString(buf, ctxErr) } *ptr = result } diff --git a/go/fory/string.go b/go/fory/string.go index 94275cb875..10586a27cd 100644 --- a/go/fory/string.go +++ b/go/fory/string.go @@ -49,17 +49,11 @@ func writeString(buf *ByteBuffer, value string) { } // readString reads a string from buffer using xlang encoding -func readString(buf *ByteBuffer, err *Error, maxBytes int) string { +func readString(buf *ByteBuffer, err *Error) string { header := buf.ReadVaruint36Small(err) size := header >> 2 // Extract byte count encoding := header & 0b11 // Extract encoding type - if maxBytes > 0 && int(size) > maxBytes { - err.SetError(DeserializationErrorf( - "fory: string byte length %d exceeds limit %d", int(size), maxBytes)) - return "" - } - switch encoding { case encodingLatin1: return readLatin1(buf, int(size), err) @@ -138,7 +132,7 @@ func (s stringSerializer) Write(ctx *WriteContext, refMode RefMode, writeType bo func (s stringSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() - str := readString(ctx.buffer, err, ctx.maxStringBytes) + str := readString(ctx.buffer, err) if ctx.HasError() { return } @@ -208,7 +202,7 @@ func (s ptrToStringSerializer) Read(ctx *ReadContext, refMode RefMode, readType func (s ptrToStringSerializer) ReadData(ctx *ReadContext, value reflect.Value) { err := ctx.Err() - str := readString(ctx.buffer, err, ctx.maxStringBytes) + str := readString(ctx.buffer, err) if ctx.HasError() { return } From aa315de015c0f293fee1cf9a895c196e4d4140f0 Mon Sep 17 00:00:00 2001 From: Zakir Date: Sun, 1 Mar 2026 15:48:53 +0530 Subject: [PATCH 4/5] fix(go): align ReadContext struct fields for gofmt compliance --- go/fory/reader.go | 1 + 1 file changed, 1 insertion(+) diff --git a/go/fory/reader.go b/go/fory/reader.go index 6b0c0cc1ca..999bc42895 100644 --- a/go/fory/reader.go +++ b/go/fory/reader.go @@ -43,6 +43,7 @@ type ReadContext struct { err Error // Accumulated error state for deferred checking lastTypePtr uintptr lastTypeInfo *TypeInfo + maxBinarySize int // Maximum byte length for a single binary payload (0 = no limit) maxCollectionSize int // Maximum element count for a single collection or map (0 = no limit) } From db1f82fa311c5166a0ea9d6d1e92073b972f72df Mon Sep 17 00:00:00 2001 From: Zakir Date: Sun, 1 Mar 2026 16:04:22 +0530 Subject: [PATCH 5/5] fix(go): remove unused checkBinarySize from ReadContext The checkBinarySize method in ReadContext was never called from any deserializer path. Per maintainer guidance, size checks are only needed at preallocation sites. The MaxBinarySize config field and WithMaxBinarySize option are retained as reserved API. --- go/fory/fory.go | 1 - go/fory/reader.go | 8 -------- 2 files changed, 9 deletions(-) diff --git a/go/fory/fory.go b/go/fory/fory.go index 43755603ca..8c97ee2e58 100644 --- a/go/fory/fory.go +++ b/go/fory/fory.go @@ -174,7 +174,6 @@ func New(opts ...Option) *Fory { f.readCtx.refResolver = f.refResolver f.readCtx.compatible = f.config.Compatible f.readCtx.xlang = f.config.IsXlang - f.readCtx.maxBinarySize = f.config.MaxBinarySize f.readCtx.maxCollectionSize = f.config.MaxCollectionSize return f diff --git a/go/fory/reader.go b/go/fory/reader.go index 999bc42895..b3d10b1142 100644 --- a/go/fory/reader.go +++ b/go/fory/reader.go @@ -44,7 +44,6 @@ type ReadContext struct { lastTypePtr uintptr lastTypeInfo *TypeInfo - maxBinarySize int // Maximum byte length for a single binary payload (0 = no limit) maxCollectionSize int // Maximum element count for a single collection or map (0 = no limit) } @@ -159,13 +158,6 @@ func (c *ReadContext) checkCollectionSize(size int) { } } -// checkBinarySize validates that a binary payload byte length does not exceed the configured limit. -func (c *ReadContext) checkBinarySize(size int) { - if c.maxBinarySize > 0 && size > c.maxBinarySize { - c.SetError(DeserializationErrorf( - "fory: binary size %d exceeds limit %d", size, c.maxBinarySize)) - } -} // Inline primitive reads func (c *ReadContext) RawBool() bool { return c.buffer.ReadBool(c.Err()) }