blob: 00a03b2d50ee71c490cc929d2a09684399d791e5 [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.
//go:build goexperiment.runtimesecret
package secret
import (
"runtime"
_ "unsafe"
)
// Do invokes f.
//
// Do ensures that any temporary storage used by f is erased in a
// timely manner. (In this context, "f" is shorthand for the
// entire call tree initiated by f.)
// - Any registers used by f are erased before Do returns.
// - Any stack used by f is erased before Do returns.
// - Heap allocations done by f are erased as soon as the garbage
// collector realizes that all allocated values are no longer reachable.
// - Do works even if f panics or calls runtime.Goexit. As part of
// that, any panic raised by f will appear as if it originates from
// Do itself.
//
// Users should be cautious of allocating inside Do.
// Erasing heap memory after Do returns may increase garbage collector sweep times and
// requires additional memory to keep track of allocations until they are to be erased.
// These costs can compound when an allocation is done in the service of growing a value,
// like appending to a slice or inserting into a map. In these cases, the entire new allocation is erased rather
// than just the secret parts of it.
//
// To reduce lifetimes of allocations and avoid unexpected performance issues,
// if a function invoked by Do needs to yield a result that shouldn't be erased,
// it should do so by copying the result into an allocation created by the caller.
//
// Limitations:
// - Currently only supported on linux/amd64 and linux/arm64. On unsupported
// platforms, Do will invoke f directly.
// - Protection does not extend to any global variables written by f.
// - Protection does not extend to any new goroutines made by f.
// - If f calls runtime.Goexit, erasure can be delayed by defers
// higher up on the call stack.
// - Heap allocations will only be erased if the program drops all
// references to those allocations, and then the garbage collector
// notices that those references are gone. The former is under
// control of the program, but the latter is at the whim of the
// runtime.
// - Any value panicked by f may point to allocations from within
// f. Those allocations will not be erased until (at least) the
// panicked value is dead.
// - Pointer addresses may leak into data buffers used by the runtime
// to perform garbage collection. Users should not encode confidential
// information into pointers. For example, if an offset into an array or
// struct is confidential, then users should not create a pointer into
// the object. Since this function is intended to be used with constant-time
// cryptographic code, this requirement is usually fulfilled implicitly.
func Do(f func()) {
const osArch = runtime.GOOS + "/" + runtime.GOARCH
switch osArch {
default:
// unsupported, just invoke f directly.
f()
return
case "linux/amd64", "linux/arm64":
}
// Place to store any panic value.
var p any
// Step 1: increment the nesting count.
inc()
// Step 2: call helper. The helper just calls f
// and captures (recovers) any panic result.
p = doHelper(f)
// Step 3: erase everything used by f (stack, registers).
eraseSecrets()
// Step 4: decrement the nesting count.
dec()
// Step 5: re-raise any caught panic.
// This will make the panic appear to come
// from a stack whose bottom frame is
// runtime/secret.Do.
// Anything below that to do with f will be gone.
//
// Note that the panic value is not erased. It behaves
// like any other value that escapes from f. If it is
// heap allocated, it will be erased when the garbage
// collector notices it is no longer referenced.
if p != nil {
panic(p)
}
// Note: if f calls runtime.Goexit, step 3 and above will not
// happen, as Goexit is unrecoverable. We handle that case in
// runtime/proc.go:goexit0.
}
func doHelper(f func()) (p any) {
// Step 2b: Pop the stack up to the secret.doHelper frame
// if we are in the process of panicking.
// (It is a no-op if we are not panicking.)
// We return any panicked value to secret.Do, who will
// re-panic it.
defer func() {
// Note: we rely on the go1.21+ behavior that
// if we are panicking, recover returns non-nil.
p = recover()
}()
// Step 2a: call the secret function.
f()
return
}
// Enabled reports whether [Do] appears anywhere on the call stack.
func Enabled() bool {
return count() > 0
}
// implemented in runtime
//go:linkname count
func count() int32
//go:linkname inc
func inc()
//go:linkname dec
func dec()
//go:linkname eraseSecrets
func eraseSecrets()