Something went wrong. Try again.
Go bindings for libghostty-vt. pkg.go.dev/go.mitchellh.com/libghostty
ghostty
Something went wrong. Try again.
go-libghostty alloc_zero_test.go
4.4 kB · 163 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164package libghostty
import ( "runtime" "sync/atomic" "testing" "unsafe")
// The tests in this file check that the package never stores a pointer// in C memory that has not been zeroed. See allocZeroed for why that// matters.//// Each test recreates the conditions for the crash on purpose. It fills// freed C blocks with a value the garbage collector treats as a bad// pointer, keeps a collection running, and then calls the code under// test. If that code skips the zeroing step, the Go runtime stops the// whole test binary with "found bad pointer in Go heap". A regression// therefore shows up as a crash, not as an ordinary test failure.
// Test inputs are sized so the arrays under test span several pages.// Some C allocators zero small blocks when they are freed, and only// larger blocks reliably come back with their old contents.const ( // uninitElems is the number of elements in each array under test. uninitElems = 1024
// uninitRounds is how many times each test repeats its check. // Without zeroing, the crash appears within the first few rounds. uninitRounds = 200)
// freedHeapAddr returns an address inside Go heap memory that has// already been released. The garbage collector reports a fatal error if// it is ever asked to follow this address.func freedHeapAddr() uintptr { // An allocation this large gets its own region of the heap, and the // whole region is released once the slice is collected. buf := make([]byte, 4<<20) addr := uintptr(unsafe.Pointer(&buf[0])) + 4096 buf = nil runtime.GC() runtime.GC() return addr}
// fillFreedBlocks allocates several C blocks of the given size, fills// them with the word v, and frees them. The next allocation of the same// size is then very likely to return a block that still holds v.func fillFreedBlocks(size, v uintptr) { var blocks [8]unsafe.Pointer for i := range blocks { blocks[i] = Alloc(size)
// uintptr is not a pointer type, so these stores are safe // even though the block has not been zeroed. words := unsafe.Slice( (*uintptr)(blocks[i]), size/unsafe.Sizeof(uintptr(0)), ) for j := range words { words[j] = v } } for _, block := range blocks { Free(block, size) }}
// runGC runs garbage collections back to back in the background until// the test ends. Pointer stores are only checked while a collection is// running, so this keeps the check active for most of the test.func runGC(t *testing.T) { t.Helper()
var stop atomic.Bool done := make(chan struct{}) go func() { defer close(done) for !stop.Load() { runtime.GC() } }() t.Cleanup(func() { stop.Store(true) <-done })}
func TestAllocZeroed(t *testing.T) { const size = 64 * 1024 fillFreedBlocks(size, ^uintptr(0))
ptr := allocZeroed(size) if ptr == nil { t.Fatal("allocZeroed returned nil") } defer Free(ptr, size) for i, b := range unsafe.Slice((*byte)(ptr), size) { if b != 0 { t.Fatalf("byte %d = %#x, want 0", i, b) } }}
func TestStringArrayUninitializedMemory(t *testing.T) { bad := freedHeapAddr() runGC(t)
// Use both empty and non-empty values. Each kind takes a different // path when it is written to the array. values := make([][]byte, uninitElems) for i := range values { if i%3 != 0 { values[i] = []byte("text/plain") } }
// Test files cannot import "C", so take the element size from the // array's own pointer type. var a cGhosttyStringArray size := uintptr(len(values)) * unsafe.Sizeof(*a.ptr)
for range uninitRounds { fillFreedBlocks(size, bad)
arr, err := newCGhosttyStringArray(values) if err != nil { t.Fatal(err) } arr.close() }}
func TestClipboardContentsUninitializedMemory(t *testing.T) { bad := freedHeapAddr() runGC(t)
values := make([]ClipboardContent, uninitElems) for i := range values { values[i] = ClipboardContent{ MIME: "text/plain", Data: []byte("hello"), } }
// Test files cannot import "C", so take the element size from the // array's own pointer type. var c cGhosttyClipboardContents size := uintptr(len(values)) * unsafe.Sizeof(*c.ptr)
for range uninitRounds { // The clipboard array and the string array behind it have // the same size in bytes, so one fill covers both. fillFreedBlocks(size, bad)
contents, err := newCGhosttyClipboardContents(values) if err != nil { t.Fatal(err) } contents.close() }}