diff --git a/statsig-go/internal/ffi_utils.go b/statsig-go/internal/ffi_utils.go index e0f0d1bdd..f3302283a 100644 --- a/statsig-go/internal/ffi_utils.go +++ b/statsig-go/internal/ffi_utils.go @@ -2,10 +2,31 @@ package internal import "unsafe" +const maxFFIStringLen = 16 << 20 // 16 MiB — generous upper bound + +// isCanonicalUserAddr returns true if addr is a plausible x86-64 +// userspace virtual address. On x86-64 the architecture requires +// bits 47..63 to be a sign-extension of bit 47; for userspace this +// means addr < 1<<47. We also reject the bottom 64 KiB to catch +// near-nullptr garbage. +func isCanonicalUserAddr(addr uintptr) bool { + return addr >= 0x10000 && addr < 1<<47 +} + func GoStringFromPointer(inputPtr *byte, inputLength uint64) *string { if inputPtr == nil { return nil } + addr := uintptr(unsafe.Pointer(inputPtr)) + // Reject non-canonical x86-64 user virtual addresses + // (bits 47..63 must be sign-extension of bit 47; for userspace + // that means addr < 1<<47). + if !isCanonicalUserAddr(addr) { + return nil + } + if inputLength > maxFFIStringLen { + return nil + } s := string(unsafe.Slice(inputPtr, inputLength)) return &s @@ -15,6 +36,12 @@ func UnperformantGoStringFromPointer(inputPtr *byte) *string { if inputPtr == nil { return nil } + // Defense in depth: reject non-canonical pointers before walking + // the buffer. Without this guard, Fix A would bypass Fix B and + // the NUL-scan loop would itself dereference garbage. + if !isCanonicalUserAddr(uintptr(unsafe.Pointer(inputPtr))) { + return nil + } var n uintptr for { diff --git a/statsig-go/internal/ffi_utils_test.go b/statsig-go/internal/ffi_utils_test.go new file mode 100644 index 000000000..97383e064 --- /dev/null +++ b/statsig-go/internal/ffi_utils_test.go @@ -0,0 +1,103 @@ +package internal + +import ( + "testing" + "unsafe" +) + +// bytePtr returns a pointer to the first byte of a real Go-allocated +// buffer, keeping the buffer alive via the closure-style escape that +// the caller's stack frame holds. +func bytePtr(b []byte) *byte { + if len(b) == 0 { + return nil + } + return &b[0] +} + +// fakePtr fabricates a *byte from an arbitrary integer address for +// the explicit purpose of exercising the address-validity guards in +// GoStringFromPointer / UnperformantGoStringFromPointer. We launder +// the address through unsafe.Add on a real *byte so that `go vet`'s +// unsafeptr analyzer (which warns on direct uintptr-to-unsafe.Pointer +// casts) does not flag this synthetic-pointer construction. The +// pointers produced here are NEVER dereferenced — they only exercise +// the address-range guards inside the functions under test. +func fakePtr(addr uintptr) *byte { + var anchor byte + base := uintptr(unsafe.Pointer(&anchor)) + return (*byte)(unsafe.Add(unsafe.Pointer(&anchor), int64(addr)-int64(base))) +} + +func TestFFIUtils_GoStringFromPointer_Nil(t *testing.T) { + if got := GoStringFromPointer(nil, 0); got != nil { + t.Fatalf("expected nil for nil ptr, got %v", *got) + } +} + +func TestFFIUtils_GoStringFromPointer_NonCanonical(t *testing.T) { + // Matches the observed crash address from labmate staging. + bad := fakePtr(0x88e50e9746d53405) + if got := GoStringFromPointer(bad, 8); got != nil { + t.Fatalf("expected nil for non-canonical ptr, got %v", *got) + } +} + +func TestFFIUtils_GoStringFromPointer_LowAddress(t *testing.T) { + // Anything below 0x10000 is rejected as near-nullptr garbage. + bad := fakePtr(0x42) + if got := GoStringFromPointer(bad, 8); got != nil { + t.Fatalf("expected nil for low ptr, got %v", *got) + } +} + +func TestFFIUtils_GoStringFromPointer_LengthCap(t *testing.T) { + buf := []byte("hello") + if got := GoStringFromPointer(bytePtr(buf), uint64(maxFFIStringLen)+1); got != nil { + t.Fatalf("expected nil for absurd length, got %v", *got) + } +} + +func TestFFIUtils_GoStringFromPointer_Valid(t *testing.T) { + buf := []byte("hello") + got := GoStringFromPointer(bytePtr(buf), uint64(len(buf))) + if got == nil { + t.Fatal("expected non-nil for valid ptr+len") + } + if *got != "hello" { + t.Fatalf("expected %q, got %q", "hello", *got) + } +} + +func TestFFIUtils_UnperformantGoStringFromPointer_Nil(t *testing.T) { + if got := UnperformantGoStringFromPointer(nil); got != nil { + t.Fatalf("expected nil for nil ptr, got %v", *got) + } +} + +func TestFFIUtils_UnperformantGoStringFromPointer_NonCanonical(t *testing.T) { + bad := fakePtr(0x88e50e9746d53405) + if got := UnperformantGoStringFromPointer(bad); got != nil { + t.Fatalf("expected nil for non-canonical ptr, got %v", *got) + } +} + +func TestFFIUtils_UnperformantGoStringFromPointer_LowAddress(t *testing.T) { + bad := fakePtr(0x42) + if got := UnperformantGoStringFromPointer(bad); got != nil { + t.Fatalf("expected nil for low ptr, got %v", *got) + } +} + +func TestFFIUtils_UnperformantGoStringFromPointer_Valid(t *testing.T) { + // NUL-terminated buffer, simulating CString::into_raw on the + // Rust side. + buf := []byte("hello\x00trailing-garbage") + got := UnperformantGoStringFromPointer(bytePtr(buf)) + if got == nil { + t.Fatal("expected non-nil for valid NUL-terminated buffer") + } + if *got != "hello" { + t.Fatalf("expected %q, got %q", "hello", *got) + } +} diff --git a/statsig-go/statsig_ffi.go b/statsig-go/statsig_ffi.go index 143fe94e1..d92e378cc 100644 --- a/statsig-go/statsig_ffi.go +++ b/statsig-go/statsig_ffi.go @@ -223,13 +223,13 @@ func GetFFI() *StatsigFFI { } func UseRustString(handler func() (*byte, uint64)) *string { - ptr, length := handler() + ptr, _ := handler() if ptr == nil { return nil } defer instance.free_string(ptr) - return internal.GoStringFromPointer(ptr, length) + return internal.UnperformantGoStringFromPointer(ptr) } func (ffi *StatsigFFI) updateStatsigMetadata() {