Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions statsig-go/internal/ffi_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
103 changes: 103 additions & 0 deletions statsig-go/internal/ffi_utils_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 2 additions & 2 deletions statsig-go/statsig_ffi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down