From 5083e4f020ef90c9c4aa4a227196c9f72dd2fed5 Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Fri, 1 May 2026 15:30:11 +0300 Subject: [PATCH] grpcreplay: reject oversized record size in readRecord to prevent OOM readRecord() reads size as a raw uint32 from the replay file and passes it directly to make([]byte, size) with no bounds check. A crafted 13-byte .grpc_replay file with size=0xFFFFFFFF triggers a 4 GB allocation in any program calling NewReplayer() on an attacker-supplied file. Add maxRecordSize (64 MiB; gRPC default max message size is 4 MiB) and reject records that exceed it before allocation. Add TestOversizeRecord to prevent regression. --- grpcreplay/binary_format.go | 8 ++++++++ grpcreplay/grpcreplay_test.go | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/grpcreplay/binary_format.go b/grpcreplay/binary_format.go index b1a1772..7309215 100644 --- a/grpcreplay/binary_format.go +++ b/grpcreplay/binary_format.go @@ -17,12 +17,17 @@ package grpcreplay import ( "encoding/binary" "errors" + "fmt" "io" pb "github.com/google/go-replayers/grpcreplay/proto/grpcreplay" "google.golang.org/protobuf/proto" ) +// maxRecordSize guards against OOM when parsing a crafted replay file. +// gRPC's default max message size is 4 MiB; 64 MiB is a generous upper bound. +const maxRecordSize = 64 << 20 // 64 MiB + type binaryWriter struct { w io.Writer } @@ -98,6 +103,9 @@ func (r *binaryReader) readRecord() ([]byte, error) { if err := binary.Read(r.r, binary.LittleEndian, &size); err != nil { return nil, err } + if size > maxRecordSize { + return nil, fmt.Errorf("grpcreplay: record size %d exceeds maximum %d", size, maxRecordSize) + } buf := make([]byte, size) if _, err := io.ReadFull(r.r, buf); err != nil { return nil, err diff --git a/grpcreplay/grpcreplay_test.go b/grpcreplay/grpcreplay_test.go index e4a5e61..81b1692 100644 --- a/grpcreplay/grpcreplay_test.go +++ b/grpcreplay/grpcreplay_test.go @@ -17,6 +17,7 @@ package grpcreplay import ( "bytes" "context" + "encoding/binary" "errors" "io" "reflect" @@ -704,3 +705,20 @@ func TestSetInitial(t *testing.T) { t.Errorf("got initial state %q, want %q", got, want) } } + +// TestOversizeRecord is a regression test for OOM via unchecked readRecord size. +// A crafted binary replay file with size > maxRecordSize must be rejected with +// an error rather than causing a large allocation. +func TestOversizeRecord(t *testing.T) { + var buf bytes.Buffer + // Write an oversize size field (maxRecordSize + 1). + binary.Write(&buf, binary.LittleEndian, uint32(maxRecordSize+1)) + + r := &binaryReader{r: &buf} + // readHeader() calls readRecord() internally. + _, err := r.readRecord() + if err == nil { + t.Fatal("readRecord should return an error for size > maxRecordSize") + } + t.Logf("correctly rejected oversized record: %v", err) +}