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

View as plain text