Source file src/runtime/secret/secret_test.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // these tests rely on inspecting freed memory, so they
     6  // can't be run under any of the memory validating modes.
     7  // TODO: figure out just which test violate which condition
     8  // and split this file out by individual test cases.
     9  // There could be some value to running some of these
    10  // under validation
    11  
    12  //go:build goexperiment.runtimesecret && (arm64 || amd64) && linux && !race && !asan && !msan
    13  
    14  package secret
    15  
    16  import (
    17  	"runtime"
    18  	"strings"
    19  	"testing"
    20  	"time"
    21  	"unsafe"
    22  	"weak"
    23  )
    24  
    25  type secretType int64
    26  
    27  const secretValue = 0x53c237_53c237
    28  
    29  // S is a type that might have some secrets in it.
    30  type S [100]secretType
    31  
    32  // makeS makes an S with secrets in it.
    33  //
    34  //go:noinline
    35  func makeS() S {
    36  	// Note: noinline ensures this doesn't get inlined and
    37  	// completely optimized away.
    38  	var s S
    39  	for i := range s {
    40  		s[i] = secretValue
    41  	}
    42  	return s
    43  }
    44  
    45  // heapS allocates an S on the heap with secrets in it.
    46  //
    47  //go:noinline
    48  func heapS() *S {
    49  	// Note: noinline forces heap allocation
    50  	s := makeS()
    51  	return &s
    52  }
    53  
    54  // for the tiny allocator
    55  //
    56  //go:noinline
    57  func heapSTiny() *secretType {
    58  	s := new(secretType(secretValue))
    59  	return s
    60  }
    61  
    62  // Test that when we allocate inside secret.Do, the resulting
    63  // allocations are zeroed by the garbage collector when they
    64  // are freed.
    65  // See runtime/mheap.go:freeSpecial.
    66  func TestHeap(t *testing.T) {
    67  	var addr uintptr
    68  	var p weak.Pointer[S]
    69  	Do(func() {
    70  		sp := heapS()
    71  		addr = uintptr(unsafe.Pointer(sp))
    72  		p = weak.Make(sp)
    73  	})
    74  	waitCollected(t, p)
    75  
    76  	// Check that object got zeroed.
    77  	checkRangeForSecret(t, addr, addr+unsafe.Sizeof(S{}))
    78  	// Also check our stack, just because we can.
    79  	checkStackForSecret(t)
    80  }
    81  
    82  func TestHeapTiny(t *testing.T) {
    83  	var addr uintptr
    84  	var p weak.Pointer[secretType]
    85  	Do(func() {
    86  		sp := heapSTiny()
    87  		addr = uintptr(unsafe.Pointer(sp))
    88  		p = weak.Make(sp)
    89  	})
    90  	waitCollected(t, p)
    91  
    92  	// Check that object got zeroed.
    93  	checkRangeForSecret(t, addr, addr+unsafe.Sizeof(secretType(0)))
    94  	// Also check our stack, just because we can.
    95  	checkStackForSecret(t)
    96  }
    97  
    98  // Test that when we return from secret.Do, we zero the stack used
    99  // by the argument to secret.Do.
   100  // See runtime/secret.go:secret_dec.
   101  func TestStack(t *testing.T) {
   102  	checkStackForSecret(t) // if this fails, something is wrong with the test
   103  
   104  	Do(func() {
   105  		s := makeS()
   106  		use(&s)
   107  	})
   108  
   109  	checkStackForSecret(t)
   110  }
   111  
   112  //go:noinline
   113  func use(s *S) {
   114  	// Note: noinline prevents dead variable elimination.
   115  }
   116  
   117  // Test that when we copy a stack, we zero the old one.
   118  // See runtime/stack.go:copystack.
   119  func TestStackCopy(t *testing.T) {
   120  	checkStackForSecret(t) // if this fails, something is wrong with the test
   121  
   122  	var lo, hi uintptr
   123  	Do(func() {
   124  		// Put some secrets on the current stack frame.
   125  		s := makeS()
   126  		use(&s)
   127  		// Remember the current stack.
   128  		lo, hi = getStack()
   129  		// Use a lot more stack to force a stack copy.
   130  		growStack()
   131  	})
   132  	checkRangeForSecret(t, lo, hi) // pre-grow stack
   133  	checkStackForSecret(t)         // post-grow stack (just because we can)
   134  }
   135  
   136  func growStack() {
   137  	growStack1(1000)
   138  }
   139  func growStack1(n int) {
   140  	if n == 0 {
   141  		return
   142  	}
   143  	growStack1(n - 1)
   144  }
   145  
   146  func TestPanic(t *testing.T) {
   147  	checkStackForSecret(t) // if this fails, something is wrong with the test
   148  
   149  	defer func() {
   150  		checkStackForSecret(t)
   151  
   152  		p := recover()
   153  		if p == nil {
   154  			t.Errorf("panic squashed")
   155  			return
   156  		}
   157  		var e error
   158  		var ok bool
   159  		if e, ok = p.(error); !ok {
   160  			t.Errorf("panic not an error")
   161  		}
   162  		if !strings.Contains(e.Error(), "divide by zero") {
   163  			t.Errorf("panic not a divide by zero error: %s", e.Error())
   164  		}
   165  		var pcs [10]uintptr
   166  		n := runtime.Callers(0, pcs[:])
   167  		frames := runtime.CallersFrames(pcs[:n])
   168  		for {
   169  			frame, more := frames.Next()
   170  			if strings.Contains(frame.Function, "dividePanic") {
   171  				t.Errorf("secret function in traceback")
   172  			}
   173  			if !more {
   174  				break
   175  			}
   176  		}
   177  	}()
   178  	Do(dividePanic)
   179  }
   180  
   181  func dividePanic() {
   182  	s := makeS()
   183  	use(&s)
   184  	_ = 8 / zero
   185  }
   186  
   187  var zero int
   188  
   189  func TestGoExit(t *testing.T) {
   190  	checkStackForSecret(t) // if this fails, something is wrong with the test
   191  
   192  	c := make(chan uintptr, 2)
   193  
   194  	go func() {
   195  		// Run the test in a separate goroutine
   196  		defer func() {
   197  			// Tell original goroutine what our stack is
   198  			// so it can check it for secrets.
   199  			lo, hi := getStack()
   200  			c <- lo
   201  			c <- hi
   202  		}()
   203  		Do(func() {
   204  			s := makeS()
   205  			use(&s)
   206  			// there's an entire round-trip through the scheduler between here
   207  			// and when we are able to check if the registers are still dirtied, and we're
   208  			// not guaranteed to run on the same M. Make a best effort attempt anyway
   209  			loadRegisters(unsafe.Pointer(&s))
   210  			runtime.Goexit()
   211  		})
   212  		t.Errorf("goexit didn't happen")
   213  	}()
   214  	lo := <-c
   215  	hi := <-c
   216  	// We want to wait until the other goroutine has finished Goexiting and
   217  	// cleared its stack. There's no signal for that, so just wait a bit.
   218  	time.Sleep(1 * time.Millisecond)
   219  
   220  	checkRangeForSecret(t, lo, hi)
   221  
   222  	var spillArea [64]secretType
   223  	n := spillRegisters(unsafe.Pointer(&spillArea))
   224  	if n > unsafe.Sizeof(spillArea) {
   225  		t.Fatalf("spill area overrun %d\n", n)
   226  	}
   227  	for i, v := range spillArea {
   228  		if v == secretValue {
   229  			t.Errorf("secret found in spill slot %d", i)
   230  		}
   231  	}
   232  }
   233  
   234  func checkStackForSecret(t *testing.T) {
   235  	t.Helper()
   236  	lo, hi := getStack()
   237  	checkRangeForSecret(t, lo, hi)
   238  }
   239  func checkRangeForSecret(t *testing.T, lo, hi uintptr) {
   240  	t.Helper()
   241  	for p := lo; p < hi; p += unsafe.Sizeof(secretType(0)) {
   242  		v := *(*secretType)(unsafe.Pointer(p))
   243  		if v == secretValue {
   244  			t.Errorf("secret found in [%x,%x] at %x", lo, hi, p)
   245  		}
   246  	}
   247  }
   248  
   249  func waitCollected[P any](t *testing.T, ptr weak.Pointer[P]) {
   250  	t.Helper()
   251  	i := 0
   252  	for ptr.Value() != nil {
   253  		runtime.GC()
   254  		i++
   255  		// 20 seems like a decent number of times to try
   256  		if i > 20 {
   257  			t.Errorf("value was never collected")
   258  		}
   259  	}
   260  	t.Logf("number of cycles until collection: %d", i)
   261  }
   262  
   263  func TestRegisters(t *testing.T) {
   264  	Do(func() {
   265  		s := makeS()
   266  		loadRegisters(unsafe.Pointer(&s))
   267  	})
   268  	var spillArea [64]secretType
   269  	n := spillRegisters(unsafe.Pointer(&spillArea))
   270  	if n > unsafe.Sizeof(spillArea) {
   271  		t.Fatalf("spill area overrun %d\n", n)
   272  	}
   273  	for i, v := range spillArea {
   274  		if v == secretValue {
   275  			t.Errorf("secret found in spill slot %d", i)
   276  		}
   277  	}
   278  }
   279  
   280  func TestSignalStacks(t *testing.T) {
   281  	Do(func() {
   282  		s := makeS()
   283  		loadRegisters(unsafe.Pointer(&s))
   284  		// cause a signal with our secret state to dirty
   285  		// at least one of the signal stacks
   286  		func() {
   287  			defer func() {
   288  				x := recover()
   289  				if x == nil {
   290  					panic("did not get panic")
   291  				}
   292  			}()
   293  			var p *int
   294  			*p = 20
   295  		}()
   296  	})
   297  	// signal stacks aren't cleared until after
   298  	// the next GC after secret.Do returns
   299  	runtime.GC()
   300  	stk := make([]stack, 0, 100)
   301  	stk = appendSignalStacks(stk)
   302  	for _, s := range stk {
   303  		checkRangeForSecret(t, s.lo, s.hi)
   304  	}
   305  }
   306  
   307  // hooks into the runtime
   308  func getStack() (uintptr, uintptr)
   309  
   310  // Stack is a copy of runtime.stack for testing export.
   311  // Fields must match.
   312  type stack struct {
   313  	lo uintptr
   314  	hi uintptr
   315  }
   316  
   317  func appendSignalStacks([]stack) []stack
   318  

View as plain text