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 b2d41923e fix(go): added pre-allocation bounds checks for slices and
strings (#3618)
b2d41923e is described below
commit b2d41923ee768f7a711c1c6b5cd78e862a1cb034
Author: Ayush Kumar <[email protected]>
AuthorDate: Sat Apr 25 17:29:12 2026 +0530
fix(go): added pre-allocation bounds checks for slices and strings (#3618)
## Why?
The code immediately allocated bytes based on the untrusted size. If any
user sent a size of 2 billion but only actually sent 5 bytes of data,
the server would instantly try to allocate 2GB of RAM. If many such
requests were sent, the server would run out of memory (OOM) and crash.
## What does this PR do?
Add check which ensures we never allocate memory for data that hasn't
actually arrived yet. It forces the allocation to be bounded by the
physical size of the data we have already received and verified.
## Related issues
Closes #3617
## 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/slice_primitive.go | 123 +++++++++++++++++++++++++++++++---------
go/fory/slice_primitive_test.go | 26 +++++++++
go/fory/string.go | 3 +
go/fory/string_test.go | 12 ++++
4 files changed, 137 insertions(+), 27 deletions(-)
diff --git a/go/fory/slice_primitive.go b/go/fory/slice_primitive.go
index dddc80069..96fe481f2 100644
--- a/go/fory/slice_primitive.go
+++ b/go/fory/slice_primitive.go
@@ -695,8 +695,11 @@ func ReadByteSlice(buf *ByteBuffer, err *Error) []byte {
if size == 0 {
return make([]byte, 0)
}
- result := make([]byte, size)
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]byte, size)
copy(result, raw)
return result
}
@@ -716,8 +719,11 @@ func ReadBoolSlice(buf *ByteBuffer, err *Error) []bool {
if size == 0 {
return make([]bool, 0)
}
- result := make([]bool, size)
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]bool, size)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])), size), raw)
return result
}
@@ -737,8 +743,11 @@ func ReadInt8Slice(buf *ByteBuffer, err *Error) []int8 {
if size == 0 {
return make([]int8, 0)
}
- result := make([]int8, size)
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]int8, size)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])), size), raw)
return result
}
@@ -765,16 +774,21 @@ func ReadInt16Slice(buf *ByteBuffer, err *Error) []int16 {
if length == 0 {
return make([]int16, 0)
}
- result := make([]int16, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]int16, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])), size),
raw)
+ return result
} else {
+ result := make([]int16, length)
for i := 0; i < length; i++ {
result[i] = buf.ReadInt16(err)
}
+ return result
}
- return result
}
// WriteInt32Slice writes []int32 to buffer using ARRAY protocol
@@ -799,16 +813,21 @@ func ReadInt32Slice(buf *ByteBuffer, err *Error) []int32 {
if length == 0 {
return make([]int32, 0)
}
- result := make([]int32, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]int32, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])), size),
raw)
+ return result
} else {
+ result := make([]int32, length)
for i := 0; i < length; i++ {
result[i] = buf.ReadInt32(err)
}
+ return result
}
- return result
}
// WriteInt64Slice writes []int64 to buffer using ARRAY protocol
@@ -833,16 +852,21 @@ func ReadInt64Slice(buf *ByteBuffer, err *Error) []int64 {
if length == 0 {
return make([]int64, 0)
}
- result := make([]int64, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]int64, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])), size),
raw)
+ return result
} else {
+ result := make([]int64, length)
for i := 0; i < length; i++ {
result[i] = buf.ReadInt64(err)
}
+ return result
}
- return result
}
// WriteUint16Slice writes []uint16 to buffer using ARRAY protocol
@@ -867,16 +891,21 @@ func ReadUint16Slice(buf *ByteBuffer, err *Error)
[]uint16 {
if length == 0 {
return make([]uint16, 0)
}
- result := make([]uint16, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]uint16, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])), size),
raw)
+ return result
} else {
+ result := make([]uint16, length)
for i := 0; i < length; i++ {
result[i] = uint16(buf.ReadInt16(err))
}
+ return result
}
- return result
}
// WriteUint32Slice writes []uint32 to buffer using ARRAY protocol
@@ -901,16 +930,21 @@ func ReadUint32Slice(buf *ByteBuffer, err *Error)
[]uint32 {
if length == 0 {
return make([]uint32, 0)
}
- result := make([]uint32, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]uint32, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])), size),
raw)
+ return result
} else {
+ result := make([]uint32, length)
for i := 0; i < length; i++ {
result[i] = uint32(buf.ReadInt32(err))
}
+ return result
}
- return result
}
// WriteUint64Slice writes []uint64 to buffer using ARRAY protocol
@@ -935,16 +969,21 @@ func ReadUint64Slice(buf *ByteBuffer, err *Error)
[]uint64 {
if length == 0 {
return make([]uint64, 0)
}
- result := make([]uint64, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]uint64, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])), size),
raw)
+ return result
} else {
+ result := make([]uint64, length)
for i := 0; i < length; i++ {
result[i] = uint64(buf.ReadInt64(err))
}
+ return result
}
- return result
}
// WriteFloat32Slice writes []float32 to buffer using ARRAY protocol
@@ -969,16 +1008,21 @@ func ReadFloat32Slice(buf *ByteBuffer, err *Error)
[]float32 {
if length == 0 {
return make([]float32, 0)
}
- result := make([]float32, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]float32, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])), size),
raw)
+ return result
} else {
+ result := make([]float32, length)
for i := 0; i < length; i++ {
result[i] = buf.ReadFloat32(err)
}
+ return result
}
- return result
}
// WriteFloat64Slice writes []float64 to buffer using ARRAY protocol
@@ -1003,16 +1047,21 @@ func ReadFloat64Slice(buf *ByteBuffer, err *Error)
[]float64 {
if length == 0 {
return make([]float64, 0)
}
- result := make([]float64, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]float64, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])), size),
raw)
+ return result
} else {
+ result := make([]float64, length)
for i := 0; i < length; i++ {
result[i] = buf.ReadFloat64(err)
}
+ return result
}
- return result
}
// ============================================================================
@@ -1137,31 +1186,41 @@ func ReadIntSlice(buf *ByteBuffer, err *Error) []int {
if length == 0 {
return make([]int, 0)
}
- result := make([]int, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]int, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])),
size), raw)
+ return result
} else {
+ result := make([]int, length)
for i := 0; i < length; i++ {
result[i] = int(buf.ReadInt64(err))
}
+ return result
}
- return result
} else {
length := size / 4
if length == 0 {
return make([]int, 0)
}
- result := make([]int, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]int, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])),
size), raw)
+ return result
} else {
+ result := make([]int, length)
for i := 0; i < length; i++ {
result[i] = int(buf.ReadInt32(err))
}
+ return result
}
- return result
}
}
@@ -1202,31 +1261,41 @@ func ReadUintSlice(buf *ByteBuffer, err *Error) []uint {
if length == 0 {
return make([]uint, 0)
}
- result := make([]uint, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]uint, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])),
size), raw)
+ return result
} else {
+ result := make([]uint, length)
for i := 0; i < length; i++ {
result[i] = uint(buf.ReadInt64(err))
}
+ return result
}
- return result
} else {
length := size / 4
if length == 0 {
return make([]uint, 0)
}
- result := make([]uint, length)
if isLittleEndian {
raw := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return nil
+ }
+ result := make([]uint, length)
copy(unsafe.Slice((*byte)(unsafe.Pointer(&result[0])),
size), raw)
+ return result
} else {
+ result := make([]uint, length)
for i := 0; i < length; i++ {
result[i] = uint(buf.ReadInt32(err))
}
+ return result
}
- return result
}
}
diff --git a/go/fory/slice_primitive_test.go b/go/fory/slice_primitive_test.go
index b072078e3..71b28df7d 100644
--- a/go/fory/slice_primitive_test.go
+++ b/go/fory/slice_primitive_test.go
@@ -451,3 +451,29 @@ func TestUint64Slice(t *testing.T) {
assert.Nil(t, result)
})
}
+
+func TestReadInt32Slice_OOM_Bug(t *testing.T) {
+ // We claim a size of 40,000 bytes, but provide no actual data.
+ buf := NewByteBuffer(nil)
+ buf.WriteLength(40000)
+
+ // Reset reader index so we can read what we just wrote
+ buf.SetReaderIndex(0)
+
+ err := &Error{}
+ result := ReadInt32Slice(buf, err)
+
+ assert.True(t, err.HasError(), "Expected an error due to out of bounds
buffer")
+ assert.Equal(t, 0, len(result), "Expected an empty slice due to missing
data")
+}
+
+func TestReadBoolSliceWrappedBuffer(t *testing.T) {
+ payload := NewByteBuffer(nil)
+ WriteBoolSlice(payload, []bool{true, false})
+
+ err := &Error{}
+ result := ReadBoolSlice(NewByteBuffer(payload.Bytes()), err)
+
+ assert.False(t, err.HasError(), "Expected wrapped buffer reads to use
the serialized payload")
+ assert.Equal(t, []bool{true, false}, result)
+}
diff --git a/go/fory/string.go b/go/fory/string.go
index 10586a27c..7c7a41bba 100644
--- a/go/fory/string.go
+++ b/go/fory/string.go
@@ -71,6 +71,9 @@ func readString(buf *ByteBuffer, err *Error) string {
// Specific encoding read methods
func readLatin1(buf *ByteBuffer, size int, err *Error) string {
data := buf.ReadBinary(size, err)
+ if err.HasError() {
+ return ""
+ }
// Latin1 bytes need to be converted to UTF-8
// Each Latin1 byte is a single Unicode code point (0-255)
runes := make([]rune, size)
diff --git a/go/fory/string_test.go b/go/fory/string_test.go
index cc39ec87a..579d5093d 100644
--- a/go/fory/string_test.go
+++ b/go/fory/string_test.go
@@ -89,3 +89,15 @@ func TestReadUTF16LE_SurrogatePair(t *testing.T) {
require.False(t, err.HasError())
require.Equal(t, "🎉", result)
}
+
+func TestReadLatin1_OOM_Bug(t *testing.T) {
+ // We claim a massive size of 10,000 bytes, but provide an empty buffer.
+ buf := NewByteBuffer(nil)
+
+ err := &Error{}
+ // readLatin1 doesn't read the length itself, it takes it as an argument
+ result := readLatin1(buf, 10000, err)
+
+ require.True(t, err.HasError(), "Expected an error due to out of bounds
buffer")
+ require.Equal(t, "", result, "Expected an empty string due to missing
data")
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]