blob: 98d67cf8a4157f1b3b2e6c5a0a2b5eef4a53d0c1 [file] [log] [blame] [edit]
// 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