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) +}