/* * * Copyright 2024 gRPC authors. * * Licensed 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 mem_test import ( "bytes" "fmt" "slices" "testing" "google.golang.org/grpc/internal" "google.golang.org/grpc/internal/grpctest" "google.golang.org/grpc/mem" ) type s struct { grpctest.Tester } func Test(t *testing.T) { internal.SetBufferPoolingThresholdForTesting.(func(int))(0) grpctest.RunSubTests(t, s{}) } // Tests that a buffer created with NewBuffer, which when later freed, invokes // the free function with the correct data. func (s) TestBuffer_NewBufferAndFree(t *testing.T) { data := "abcd" freed := false freeF := poolFunc(func(got *[]byte) { if !bytes.Equal(*got, []byte(data)) { t.Fatalf("Free function called with bytes %s, want %s", string(*got), data) } freed = true }) buf := newBuffer([]byte(data), freeF) if got := buf.ReadOnlyData(); !bytes.Equal(got, []byte(data)) { t.Fatalf("Buffer contains data %s, want %s", string(got), string(data)) } // Verify that the free function is invoked when all references are freed. buf.Free() if !freed { t.Fatalf("Buffer not freed") } } // Tests that a buffer created with NewBuffer, on which an additional reference // is acquired, which when later freed, invokes the free function with the // correct data, but only after all references are released. func (s) TestBuffer_NewBufferRefAndFree(t *testing.T) { data := "abcd" freed := false freeF := poolFunc(func(got *[]byte) { if !bytes.Equal(*got, []byte(data)) { t.Fatalf("Free function called with bytes %s, want %s", string(*got), string(data)) } freed = true }) buf := newBuffer([]byte(data), freeF) if got := buf.ReadOnlyData(); !bytes.Equal(got, []byte(data)) { t.Fatalf("Buffer contains data %s, want %s", string(got), string(data)) } buf.Ref() if got := buf.ReadOnlyData(); !bytes.Equal(got, []byte(data)) { t.Fatalf("New reference to the Buffer contains data %s, want %s", string(got), string(data)) } // Verify that the free function is not invoked when all references are yet // to be freed. buf.Free() if freed { t.Fatalf("Free function called before all references freed") } // Verify that the free function is invoked when all references are freed. buf.Free() if !freed { t.Fatalf("Buffer not freed") } } func (s) TestBuffer_NewBufferHandlesShortBuffers(t *testing.T) { const threshold = 100 // Update the pooling threshold, since that's what's being tested. internal.SetBufferPoolingThresholdForTesting.(func(int))(threshold) t.Cleanup(func() { internal.SetBufferPoolingThresholdForTesting.(func(int))(0) }) // Make a pool with a buffer whose capacity is larger than the pooling // threshold, but whose length is less than the threshold. b := make([]byte, threshold/2, threshold*2) pool := &singleBufferPool{ t: t, data: &b, } // Get a Buffer, then free it. If NewBuffer decided that the Buffer // shouldn't get pooled, Free will be a noop and singleBufferPool will not // have been updated. mem.NewBuffer(&b, pool).Free() if pool.data != nil { t.Fatalf("Buffer not returned to pool") } } func (s) TestBuffer_FreeAfterFree(t *testing.T) { buf := newBuffer([]byte("abcd"), mem.NopBufferPool{}) if buf.Len() != 4 { t.Fatalf("Buffer length is %d, want 4", buf.Len()) } // Ensure that a double free does panic. buf.Free() defer checkForPanic(t, "Cannot free freed buffer") buf.Free() } type singleBufferPool struct { t *testing.T data *[]byte } func (s *singleBufferPool) Get(length int) *[]byte { if len(*s.data) != length { s.t.Fatalf("Invalid requested length, got %d want %d", length, len(*s.data)) } return s.data } func (s *singleBufferPool) Put(b *[]byte) { if s.data != b { s.t.Fatalf("Wrong buffer returned to pool, got %p want %p", b, s.data) } s.data = nil } // Tests that a buffer created with Copy, which when later freed, returns the underlying // byte slice to the buffer pool. func (s) TestBuffer_CopyAndFree(t *testing.T) { data := []byte("abcd") testPool := &singleBufferPool{ t: t, data: &data, } buf := mem.Copy(data, testPool) if got := buf.ReadOnlyData(); !bytes.Equal(got, data) { t.Fatalf("Buffer contains data %s, want %s", string(got), string(data)) } // Verify that the free function is invoked when all references are freed. buf.Free() if testPool.data != nil { t.Fatalf("Buffer not freed") } } // Tests that a buffer created with Copy, on which an additional reference is // acquired, which when later freed, returns the underlying byte slice to the // buffer pool. func (s) TestBuffer_CopyRefAndFree(t *testing.T) { data := []byte("abcd") testPool := &singleBufferPool{ t: t, data: &data, } buf := mem.Copy(data, testPool) if got := buf.ReadOnlyData(); !bytes.Equal(got, data) { t.Fatalf("Buffer contains data %s, want %s", string(got), string(data)) } buf.Ref() if got := buf.ReadOnlyData(); !bytes.Equal(got, []byte(data)) { t.Fatalf("New reference to the Buffer contains data %s, want %s", string(got), string(data)) } // Verify that the free function is not invoked when all references are yet // to be freed. buf.Free() if testPool.data == nil { t.Fatalf("Free function called before all references freed") } // Verify that the free function is invoked when all references are freed. buf.Free() if testPool.data != nil { t.Fatalf("Free never called") } } func (s) TestBuffer_ReadOnlyDataAfterFree(t *testing.T) { // Verify that reading before freeing does not panic. buf := newBuffer([]byte("abcd"), mem.NopBufferPool{}) buf.ReadOnlyData() buf.Free() defer checkForPanic(t, "Cannot read freed buffer") buf.ReadOnlyData() } func (s) TestBuffer_RefAfterFree(t *testing.T) { // Verify that acquiring a ref before freeing does not panic. buf := newBuffer([]byte("abcd"), mem.NopBufferPool{}) buf.Ref() // This first call should not panic and bring the ref counter down to 1 buf.Free() // This second call actually frees the buffer buf.Free() defer checkForPanic(t, "Cannot ref freed buffer") buf.Ref() } func (s) TestBuffer_SplitAfterFree(t *testing.T) { // Verify that splitting before freeing does not panic. buf := newBuffer([]byte("abcd"), mem.NopBufferPool{}) buf, bufSplit := mem.SplitUnsafe(buf, 2) bufSplit.Free() buf.Free() defer checkForPanic(t, "Cannot split freed buffer") mem.SplitUnsafe(buf, 2) } type poolFunc func(*[]byte) func (p poolFunc) Get(int) *[]byte { panic("Get should never be called") } func (p poolFunc) Put(i *[]byte) { p(i) } func (s) TestBuffer_Split(t *testing.T) { freed := make(chan struct{}) data := []byte{1, 2, 3, 4} buf := mem.NewBuffer(&data, poolFunc(func(*[]byte) { close(freed) })) buf, split1 := mem.SplitUnsafe(buf, 2) if !bytes.Equal(buf.ReadOnlyData(), data[:2]) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", buf.ReadOnlyData(), data[:2]) } if !bytes.Equal(split1.ReadOnlyData(), data[2:]) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", split1.ReadOnlyData(), data[2:]) } // Check that splitting the buffer more than once works as intended. split1, split2 := mem.SplitUnsafe(split1, 1) if !bytes.Equal(split1.ReadOnlyData(), data[2:3]) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", split1.ReadOnlyData(), data[2:3]) } if !bytes.Equal(split2.ReadOnlyData(), data[3:]) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", split2.ReadOnlyData(), data[3:]) } // If any of the following frees actually free the buffer, the test will fail. buf.Free() split2.Free() select { case <-freed: t.Fatalf("Freed too early") default: } split1.Free() select { case <-freed: default: t.Fatalf("Still not freed") } } func (s) TestBuffer_Slice(t *testing.T) { freed := make(chan struct{}) data := []byte{1, 2, 3, 4} buf := mem.NewBuffer(&data, poolFunc(func(*[]byte) { close(freed) })) // Slice the buffer and verify the data. slice1 := buf.Slice(1, 3) if !bytes.Equal(slice1.ReadOnlyData(), data[1:3]) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", slice1.ReadOnlyData(), data[1:3]) } // Verify the original buffer is not modified. if !bytes.Equal(buf.ReadOnlyData(), data) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", buf.ReadOnlyData(), data) } // Slice the slice. slice2 := slice1.Slice(0, 1) if !bytes.Equal(slice2.ReadOnlyData(), data[1:2]) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", slice2.ReadOnlyData(), data[1:2]) } // Free original and first slice — root should not be freed yet. buf.Free() slice1.Free() select { case <-freed: t.Fatalf("Freed too early") default: } // The last slice keeps the root alive. if !bytes.Equal(slice2.ReadOnlyData(), data[1:2]) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", slice2.ReadOnlyData(), data[1:2]) } slice2.Free() select { case <-freed: default: t.Fatalf("Still not freed") } } func (s) TestBuffer_SliceAfterFree(t *testing.T) { buf := newBuffer([]byte("abcd"), mem.NopBufferPool{}) buf.Free() defer checkForPanic(t, "Cannot slice freed buffer") buf.Slice(0, 2) } type namedCtor struct { name string newBuf func([]byte) mem.Buffer } var ctors = []namedCtor{ {name: "pooled", newBuf: newPooledBuffer}, {name: "slice", newBuf: newSliceBuf}, } func (s) TestBuffer_SliceBasic(t *testing.T) { type sliceCase struct { start, end int want []byte } cases := []sliceCase{ {1, 3, []byte{2, 3}}, {0, 4, []byte{1, 2, 3, 4}}, {4, 4, []byte{}}, } for _, c := range ctors { for _, tc := range cases { t.Run(fmt.Sprintf("%s[%d:%d]", c.name, tc.start, tc.end), func(t *testing.T) { buf := c.newBuf([]byte{1, 2, 3, 4}) got := buf.Slice(tc.start, tc.end) if !bytes.Equal(got.ReadOnlyData(), tc.want) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", got.ReadOnlyData(), tc.want) } }) } } } func (s) TestBuffer_SliceSubslice(t *testing.T) { for _, c := range ctors { t.Run(c.name+" subslice", func(t *testing.T) { buf := c.newBuf([]byte{1, 2, 3, 4}) slice := buf.Slice(1, 3) slice2 := slice.Slice(0, 1) if !bytes.Equal(slice2.ReadOnlyData(), []byte{2}) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", buf.ReadOnlyData(), []byte{2}) } }) } } func (s) TestBuffer_SliceEmpty(t *testing.T) { allCtors := append(slices.Clip(ctors), namedCtor{name: "empty", newBuf: newEmptyBuf}) for _, c := range allCtors { t.Run(c.name, func(t *testing.T) { buf := c.newBuf([]byte{1, 2, 3, 4}) got := buf.Slice(0, 0) if !bytes.Equal(got.ReadOnlyData(), nil) { t.Fatalf("Buffer did not contain expected data. got %v, want %v", got.ReadOnlyData(), nil) } }) } } func (s) TestBuffer_SliceBoundsCheck(t *testing.T) { type panicCase struct { name string start, end int } nonEmptyCases := []panicCase{ {"end_out_of_bounds", 0, 5}, {"start_negative", -1, 3}, {"start_greater_than_end", 3, 0}, } tests := []struct { namedCtor panicCases []panicCase }{ { namedCtor: namedCtor{ name: "buffer", newBuf: newPooledBuffer, }, panicCases: nonEmptyCases, }, { namedCtor: namedCtor{ name: "SliceBuffer", newBuf: newSliceBuf, }, panicCases: nonEmptyCases, }, { namedCtor: namedCtor{ name: "emptyBuffer", newBuf: newEmptyBuf, }, panicCases: []panicCase{ {"end_out_of_bounds", 0, 1}, {"start_negative", -1, 0}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for _, pc := range tt.panicCases { t.Run(pc.name, func(t *testing.T) { buf := tt.newBuf([]byte{1, 2, 3, 4}) defer func() { if recover() == nil { t.Fatalf("Slice(%d, %d) did not panic", pc.start, pc.end) } }() buf.Slice(pc.start, pc.end) }) } }) } } func newPooledBuffer(data []byte) mem.Buffer { return newBuffer(data, mem.NopBufferPool{}) } func newSliceBuf(data []byte) mem.Buffer { return mem.SliceBuffer(data) } func newEmptyBuf([]byte) mem.Buffer { var bs mem.BufferSlice return bs.MaterializeToBuffer(mem.NopBufferPool{}) } func checkForPanic(t *testing.T, wantErr string) { t.Helper() r := recover() if r == nil { t.Fatalf("Use after free did not panic") } if msg, ok := r.(string); !ok || msg != wantErr { t.Fatalf("panic called with %v, want %s", r, wantErr) } }