From b5b9929c624408f0a79f79b6735cfddb8a0155f1 Mon Sep 17 00:00:00 2001 From: YLChen-007 <1561316811@qq.com> Date: Sun, 12 Apr 2026 10:55:06 +0800 Subject: [PATCH] Fix DoS via panic in Go GetRootAs functions on short buffers Add bounds checking to GetRootAs, GetSizePrefixedRootAs, GetSizePrefix, GetIndirectOffset, and GetBufferIdentifier in go/lib.go to prevent panics when called with buffers shorter than SizeUOffsetT (4 bytes). Also update the flatc Go code generator (idl_gen_go.cpp) so that all generated GetRootAs and GetSizePrefixedRootAs functions include the same bounds check, returning nil on short buffers instead of panicking. This extends the fix previously applied only to the gRPC path (grpc.go, PR #8684) to the general-purpose API. --- go/lib.go | 24 +++++++++ go/lib_test.go | 118 +++++++++++++++++++++++++++++++++++++++++++++ src/idl_gen_go.cpp | 9 ++++ 3 files changed, 151 insertions(+) create mode 100644 go/lib_test.go diff --git a/go/lib.go b/go/lib.go index a4e99de101..1bb1a337da 100644 --- a/go/lib.go +++ b/go/lib.go @@ -1,5 +1,11 @@ package flatbuffers +import "errors" + +// ErrBufferTooShort is returned when the buffer is too short to read the +// root table offset (UOffsetT). +var ErrBufferTooShort = errors.New("flatbuffers: buffer too short") + // FlatBuffer is the interface that represents a flatbuffer. type FlatBuffer interface { Table() Table @@ -8,6 +14,9 @@ type FlatBuffer interface { // GetRootAs is a generic helper to initialize a FlatBuffer with the provided buffer bytes and its data offset. func GetRootAs(buf []byte, offset UOffsetT, fb FlatBuffer) { + if int(offset)+SizeUOffsetT > len(buf) { + return + } n := GetUOffsetT(buf[offset:]) fb.Init(buf, n+offset) } @@ -15,27 +24,42 @@ func GetRootAs(buf []byte, offset UOffsetT, fb FlatBuffer) { // GetSizePrefixedRootAs is a generic helper to initialize a FlatBuffer with the provided size-prefixed buffer // bytes and its data offset func GetSizePrefixedRootAs(buf []byte, offset UOffsetT, fb FlatBuffer) { + if int(offset)+sizePrefixLength+SizeUOffsetT > len(buf) { + return + } n := GetUOffsetT(buf[offset+sizePrefixLength:]) fb.Init(buf, n+offset+sizePrefixLength) } // GetSizePrefix reads the size from a size-prefixed flatbuffer func GetSizePrefix(buf []byte, offset UOffsetT) uint32 { + if int(offset)+SizeUOffsetT > len(buf) { + return 0 + } return GetUint32(buf[offset:]) } // GetIndirectOffset retrives the relative offset in the provided buffer stored at `offset`. func GetIndirectOffset(buf []byte, offset UOffsetT) UOffsetT { + if int(offset)+SizeUOffsetT > len(buf) { + return 0 + } return offset + GetUOffsetT(buf[offset:]) } // GetBufferIdentifier returns the file identifier as string func GetBufferIdentifier(buf []byte) string { + if len(buf) < SizeUOffsetT+fileIdentifierLength { + return "" + } return string(buf[SizeUOffsetT:][:fileIdentifierLength]) } // GetBufferIdentifier returns the file identifier as string for a size-prefixed buffer func GetSizePrefixedBufferIdentifier(buf []byte) string { + if len(buf) < SizeUOffsetT+int(sizePrefixLength)+fileIdentifierLength { + return "" + } return string(buf[SizeUOffsetT+sizePrefixLength:][:fileIdentifierLength]) } diff --git a/go/lib_test.go b/go/lib_test.go new file mode 100644 index 0000000000..b5abdc0a4d --- /dev/null +++ b/go/lib_test.go @@ -0,0 +1,118 @@ +package flatbuffers + +import "testing" + +// TestGetRootAsShortBuffer verifies that GetRootAs does not panic when +// given a buffer shorter than SizeUOffsetT (4 bytes). +func TestGetRootAsShortBuffer(t *testing.T) { + shortBuffers := [][]byte{ + nil, + {}, + {0x01}, + {0x01, 0x02}, + {0x01, 0x02, 0x03}, + } + for _, buf := range shortBuffers { + // Must not panic + tab := &Table{} + GetRootAs(buf, 0, tab) + } +} + +// TestGetSizePrefixedRootAsShortBuffer verifies that GetSizePrefixedRootAs +// does not panic when given a buffer shorter than the required minimum. +func TestGetSizePrefixedRootAsShortBuffer(t *testing.T) { + shortBuffers := [][]byte{ + nil, + {}, + {0x01, 0x02, 0x03}, + {0x01, 0x02, 0x03, 0x04}, // 4 bytes: only size prefix, no root offset + {0x01, 0x02, 0x03, 0x04, 0x05}, // 5 bytes: still too short for prefix + UOffsetT + {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, // 7 bytes: still too short + } + for _, buf := range shortBuffers { + // Must not panic + tab := &Table{} + GetSizePrefixedRootAs(buf, 0, tab) + } +} + +// TestGetSizePrefixShortBuffer verifies GetSizePrefix doesn't panic on +// buffers shorter than 4 bytes. +func TestGetSizePrefixShortBuffer(t *testing.T) { + shortBuffers := [][]byte{ + nil, + {}, + {0x01}, + {0x01, 0x02, 0x03}, + } + for _, buf := range shortBuffers { + result := GetSizePrefix(buf, 0) + if result != 0 { + t.Errorf("GetSizePrefix on short buffer should return 0, got %d", result) + } + } +} + +// TestGetIndirectOffsetShortBuffer verifies GetIndirectOffset doesn't panic +// on buffers shorter than 4 bytes. +func TestGetIndirectOffsetShortBuffer(t *testing.T) { + shortBuffers := [][]byte{ + nil, + {}, + {0x01, 0x02, 0x03}, + } + for _, buf := range shortBuffers { + result := GetIndirectOffset(buf, 0) + if result != 0 { + t.Errorf("GetIndirectOffset on short buffer should return 0, got %d", result) + } + } +} + +// TestGetBufferIdentifierShortBuffer verifies GetBufferIdentifier doesn't +// panic on buffers shorter than SizeUOffsetT + fileIdentifierLength. +func TestGetBufferIdentifierShortBuffer(t *testing.T) { + shortBuffers := [][]byte{ + nil, + {}, + {0x01, 0x02, 0x03}, + {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, // 7 bytes: need at least 8 + } + for _, buf := range shortBuffers { + result := GetBufferIdentifier(buf) + if result != "" { + t.Errorf("GetBufferIdentifier on short buffer should return empty, got %q", result) + } + } +} + +// TestGetRootAsValidBuffer ensures that GetRootAs still works correctly +// for valid buffers. +func TestGetRootAsValidBuffer(t *testing.T) { + // Create a minimal valid buffer: a 4-byte UOffsetT pointing to offset 4 + buf := []byte{0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + tab := &Table{} + GetRootAs(buf, 0, tab) + if tab.Pos != 4 { + t.Errorf("Expected tab.Pos to be 4, got %d", tab.Pos) + } +} + +// TestGetRootAsWithOffset verifies bounds checking works with non-zero offset. +func TestGetRootAsWithOffset(t *testing.T) { + // Buffer with offset=2 needs at least 6 bytes (offset + SizeUOffsetT) + buf := []byte{0x00, 0x00, 0x04, 0x00, 0x00, 0x00} + tab := &Table{} + GetRootAs(buf, 2, tab) + if tab.Pos != 6 { + t.Errorf("Expected tab.Pos to be 6, got %d", tab.Pos) + } + + // Same buffer but with offset=3 should silently fail (only 3 bytes remain) + tab2 := &Table{} + GetRootAs(buf, 3, tab2) + if tab2.Pos != 0 { + t.Errorf("Expected tab.Pos to be 0 for short buffer, got %d", tab2.Pos) + } +} diff --git a/src/idl_gen_go.cpp b/src/idl_gen_go.cpp index a4a734588c..70cecad697 100644 --- a/src/idl_gen_go.cpp +++ b/src/idl_gen_go.cpp @@ -328,8 +328,17 @@ class GoGenerator : public BaseGenerator { code += "*" + struct_type + ""; code += " {\n"; if (i == 0) { + code += + "\tif int(offset)+flatbuffers.SizeUOffsetT > len(buf) {\n" + "\t\treturn nil\n" + "\t}\n"; code += "\tn := flatbuffers.GetUOffsetT(buf[offset:])\n"; } else { + code += + "\tif int(offset)+flatbuffers.SizeUint32+" + "flatbuffers.SizeUOffsetT > len(buf) {\n" + "\t\treturn nil\n" + "\t}\n"; code += "\tn := " "flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])\n";