| // Copyright 2024 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // these tests rely on inspecting freed memory, so they |
| // can't be run under any of the memory validating modes. |
| // TODO: figure out just which test violate which condition |
| // and split this file out by individual test cases. |
| // There could be some value to running some of these |
| // under validation |
| |
| //go:build goexperiment.runtimesecret && (arm64 || amd64) && linux && !race && !asan && !msan |
| |
| package secret |
| |
| import ( |
| "runtime" |
| "strings" |
| "testing" |
| "time" |
| "unsafe" |
| ) |
| |
| type secretType int64 |
| |
| const secretValue = 0x53c237_53c237 |
| |
| // S is a type that might have some secrets in it. |
| type S [100]secretType |
| |
| // makeS makes an S with secrets in it. |
| // |
| //go:noinline |
| func makeS() S { |
| // Note: noinline ensures this doesn't get inlined and |
| // completely optimized away. |
| var s S |
| for i := range s { |
| s[i] = secretValue |
| } |
| return s |
| } |
| |
| // heapS allocates an S on the heap with secrets in it. |
| // |
| //go:noinline |
| func heapS() *S { |
| // Note: noinline forces heap allocation |
| s := makeS() |
| return &s |
| } |
| |
| // for the tiny allocator |
| // |
| //go:noinline |
| func heapSTiny() *secretType { |
| s := new(secretType(secretValue)) |
| return s |
| } |
| |
| // Test that when we allocate inside secret.Do, the resulting |
| // allocations are zeroed by the garbage collector when they |
| // are freed. |
| // See runtime/mheap.go:freeSpecial. |
| func TestHeap(t *testing.T) { |
| var u uintptr |
| Do(func() { |
| u = uintptr(unsafe.Pointer(heapS())) |
| }) |
| |
| runtime.GC() |
| |
| // Check that object got zeroed. |
| checkRangeForSecret(t, u, u+unsafe.Sizeof(S{})) |
| // Also check our stack, just because we can. |
| checkStackForSecret(t) |
| } |
| |
| func TestHeapTiny(t *testing.T) { |
| var u uintptr |
| Do(func() { |
| u = uintptr(unsafe.Pointer(heapSTiny())) |
| }) |
| runtime.GC() |
| |
| // Check that object got zeroed. |
| checkRangeForSecret(t, u, u+unsafe.Sizeof(secretType(0))) |
| // Also check our stack, just because we can. |
| checkStackForSecret(t) |
| } |
| |
| // Test that when we return from secret.Do, we zero the stack used |
| // by the argument to secret.Do. |
| // See runtime/secret.go:secret_dec. |
| func TestStack(t *testing.T) { |
| checkStackForSecret(t) // if this fails, something is wrong with the test |
| |
| Do(func() { |
| s := makeS() |
| use(&s) |
| }) |
| |
| checkStackForSecret(t) |
| } |
| |
| //go:noinline |
| func use(s *S) { |
| // Note: noinline prevents dead variable elimination. |
| } |
| |
| // Test that when we copy a stack, we zero the old one. |
| // See runtime/stack.go:copystack. |
| func TestStackCopy(t *testing.T) { |
| checkStackForSecret(t) // if this fails, something is wrong with the test |
| |
| var lo, hi uintptr |
| Do(func() { |
| // Put some secrets on the current stack frame. |
| s := makeS() |
| use(&s) |
| // Remember the current stack. |
| lo, hi = getStack() |
| // Use a lot more stack to force a stack copy. |
| growStack() |
| }) |
| checkRangeForSecret(t, lo, hi) // pre-grow stack |
| checkStackForSecret(t) // post-grow stack (just because we can) |
| } |
| |
| func growStack() { |
| growStack1(1000) |
| } |
| func growStack1(n int) { |
| if n == 0 { |
| return |
| } |
| growStack1(n - 1) |
| } |
| |
| func TestPanic(t *testing.T) { |
| checkStackForSecret(t) // if this fails, something is wrong with the test |
| |
| defer func() { |
| checkStackForSecret(t) |
| |
| p := recover() |
| if p == nil { |
| t.Errorf("panic squashed") |
| return |
| } |
| var e error |
| var ok bool |
| if e, ok = p.(error); !ok { |
| t.Errorf("panic not an error") |
| } |
| if !strings.Contains(e.Error(), "divide by zero") { |
| t.Errorf("panic not a divide by zero error: %s", e.Error()) |
| } |
| var pcs [10]uintptr |
| n := runtime.Callers(0, pcs[:]) |
| frames := runtime.CallersFrames(pcs[:n]) |
| for { |
| frame, more := frames.Next() |
| if strings.Contains(frame.Function, "dividePanic") { |
| t.Errorf("secret function in traceback") |
| } |
| if !more { |
| break |
| } |
| } |
| }() |
| Do(dividePanic) |
| } |
| |
| func dividePanic() { |
| s := makeS() |
| use(&s) |
| _ = 8 / zero |
| } |
| |
| var zero int |
| |
| func TestGoExit(t *testing.T) { |
| checkStackForSecret(t) // if this fails, something is wrong with the test |
| |
| c := make(chan uintptr, 2) |
| |
| go func() { |
| // Run the test in a separate goroutine |
| defer func() { |
| // Tell original goroutine what our stack is |
| // so it can check it for secrets. |
| lo, hi := getStack() |
| c <- lo |
| c <- hi |
| }() |
| Do(func() { |
| s := makeS() |
| use(&s) |
| // there's an entire round-trip through the scheduler between here |
| // and when we are able to check if the registers are still dirtied, and we're |
| // not guaranteed to run on the same M. Make a best effort attempt anyway |
| loadRegisters(unsafe.Pointer(&s)) |
| runtime.Goexit() |
| }) |
| t.Errorf("goexit didn't happen") |
| }() |
| lo := <-c |
| hi := <-c |
| // We want to wait until the other goroutine has finished Goexiting and |
| // cleared its stack. There's no signal for that, so just wait a bit. |
| time.Sleep(1 * time.Millisecond) |
| |
| checkRangeForSecret(t, lo, hi) |
| |
| var spillArea [64]secretType |
| n := spillRegisters(unsafe.Pointer(&spillArea)) |
| if n > unsafe.Sizeof(spillArea) { |
| t.Fatalf("spill area overrun %d\n", n) |
| } |
| for i, v := range spillArea { |
| if v == secretValue { |
| t.Errorf("secret found in spill slot %d", i) |
| } |
| } |
| } |
| |
| func checkStackForSecret(t *testing.T) { |
| t.Helper() |
| lo, hi := getStack() |
| checkRangeForSecret(t, lo, hi) |
| } |
| func checkRangeForSecret(t *testing.T, lo, hi uintptr) { |
| t.Helper() |
| for p := lo; p < hi; p += unsafe.Sizeof(secretType(0)) { |
| v := *(*secretType)(unsafe.Pointer(p)) |
| if v == secretValue { |
| t.Errorf("secret found in [%x,%x] at %x", lo, hi, p) |
| } |
| } |
| } |
| |
| func TestRegisters(t *testing.T) { |
| Do(func() { |
| s := makeS() |
| loadRegisters(unsafe.Pointer(&s)) |
| }) |
| var spillArea [64]secretType |
| n := spillRegisters(unsafe.Pointer(&spillArea)) |
| if n > unsafe.Sizeof(spillArea) { |
| t.Fatalf("spill area overrun %d\n", n) |
| } |
| for i, v := range spillArea { |
| if v == secretValue { |
| t.Errorf("secret found in spill slot %d", i) |
| } |
| } |
| } |
| |
| func TestSignalStacks(t *testing.T) { |
| Do(func() { |
| s := makeS() |
| loadRegisters(unsafe.Pointer(&s)) |
| // cause a signal with our secret state to dirty |
| // at least one of the signal stacks |
| func() { |
| defer func() { |
| x := recover() |
| if x == nil { |
| panic("did not get panic") |
| } |
| }() |
| var p *int |
| *p = 20 |
| }() |
| }) |
| // signal stacks aren't cleared until after |
| // the next GC after secret.Do returns |
| runtime.GC() |
| stk := make([]stack, 0, 100) |
| stk = appendSignalStacks(stk) |
| for _, s := range stk { |
| checkRangeForSecret(t, s.lo, s.hi) |
| } |
| } |
| |
| // hooks into the runtime |
| func getStack() (uintptr, uintptr) |
| |
| // Stack is a copy of runtime.stack for testing export. |
| // Fields must match. |
| type stack struct { |
| lo uintptr |
| hi uintptr |
| } |
| |
| func appendSignalStacks([]stack) []stack |