Go Proposal: Enhanced Memory Security with `runtime/secret` "Secret Mode"
The new Go `runtime/secret` package introduces a "secret mode" to automatically erase sensitive data from memory, including registers, stack, and heap, after cryptographic operations, significantly enhancing security and preventing secret leaks.
The Go language is introducing a new runtime/secret package, enabling a "secret mode" to automatically erase sensitive data from memory. This feature, part of upcoming Go changes, is designed to enhance security by ensuring that temporary secrets, particularly those used in cryptographic operations, are promptly cleared from registers, stack, and heap.
Summary of runtime/secret
The runtime/secret package allows developers to execute a function within a secret mode. Once the function completes, either normally or due to a panic, the memory it utilized—specifically, registers and stack—is immediately zeroed out. Heap allocations made during the function's execution are also erased as soon as the garbage collector determines they are no longer reachable.
This mechanism ensures that sensitive information does not persist in memory longer than absolutely necessary, thereby significantly reducing the window of opportunity for attackers to access it. While a powerful addition, the package is currently experimental and primarily intended for developers building cryptographic libraries, rather than general application developers.
Motivation for Secret Mode
Cryptographic protocols, such as WireGuard or TLS, rely on "forward secrecy." This property ensures that even if long-term secrets (like a private key) are compromised, past communication sessions cannot be decrypted. Achieving forward secrecy necessitates the reliable erasure of session keys and other temporary secrets from memory after their use. Without a robust method to clear this memory, these keys could remain accessible indefinitely, undermining the very foundation of forward secrecy.
In Go, the runtime manages memory, and there are no inherent guarantees regarding the immediate clearing of allocated memory. Sensitive data might linger in heap allocations or stack frames, making it vulnerable to exposure through core dumps or advanced memory scanning attacks. Historically, developers have resorted to unreliable "hacks" involving reflection to zero out internal buffers in cryptographic libraries, often with limited success and without full control over all memory regions.
The runtime/secret package addresses this critical gap by providing a runtime-level mechanism that automatically erases all temporary storage used during sensitive operations. This streamlines the development of secure cryptographic libraries, eliminating the need for complex and often incomplete workarounds.
runtime/secret Description and Usage
The runtime/secret package introduces two key functions: Do and Enabled.
-
func Do(f func()): Invokes functionfin secret mode.Doguarantees that any temporary storage used byf(and its entire call tree) is erased in a timely manner.- Registers used by
fare erased beforeDoreturns. - Stack used by
fis erased beforeDoreturns. - Heap allocations made by
fare erased as soon as the garbage collector determines they are no longer reachable. Dohandles panics andruntime.Goexit, ensuring erasure even in exceptional scenarios, with panics appearing to originate fromDoitself.
- Registers used by
-
func Enabled() bool: Reports whetherDois active anywhere on the current call stack.
Current Limitations
It's important to be aware of the runtime/secret package's current limitations:
- Platform Support: Currently supported only on
linux/amd64andlinux/arm64. On unsupported platforms,Dosimply invokesfdirectly without memory erasure guarantees. - Global Variables: Protection does not extend to any global variables that
fwrites to. - Goroutine Creation: Attempting to start a new goroutine within the function
fpassed toDowill cause a panic. runtime.Goexit: Iffcallsruntime.Goexit, memory erasure is delayed until all deferred functions have completed.- Heap Erasure Timing: Heap allocations are only erased if all references to them are dropped by the program AND the garbage collector subsequently notices that these references are gone. While the program controls the first part, the second part depends on the runtime's garbage collection schedule.
- Panicked Values: If
fpanics, the panicked value might retain references to memory allocated withinf. This memory will not be erased until the panicked value itself is no longer reachable. - Pointer Address Leaks: A critical consideration is that pointer addresses might leak into internal data buffers used by the garbage collector. Developers must not store confidential information directly within pointers. For instance, if a secret offset
100in adataarray (data[100]) is confidential, creating a pointerpto&data[100]could expose the offset through the GC's internal memory, which might be targeted by attackers.
The runtime/secret package is primarily designed for developers working on cryptographic libraries. Most general applications should instead utilize higher-level libraries that implement secret.Do internally, abstracting away these complexities.
As of Go 1.26, runtime/secret is an experimental package. It can be enabled at build time by setting the environment variable GOEXPERIMENT=runtimesecret.
Example: Encrypting Data with secret.Do
The following example demonstrates how secret.Do can be used to generate an ephemeral key and encrypt a message using AES-GCM, ensuring that sensitive key material and internal cipher state are securely erased from memory:
// Encrypt generates an ephemeral key and encrypts the message.
// It wraps the entire sensitive operation in secret.Do to ensure
// the key and internal AES state are erased from memory.
func Encrypt(message []byte) ([]byte, error) {
var ciphertext []byte
var encErr error
secret.Do(func() {
// 1. Generate an ephemeral 32-byte key.
// This allocation is protected by secret.Do.
key := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
encErr = err
return
}
// 2. Create the cipher (expands key into round keys).
// This structure is also protected.
block, err := aes.NewCipher(key)
if err != nil {
encErr = err
return
}
gcm, err := cipher.NewGCM(block)
if err != nil {
encErr = err
return
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
encErr = err
return
}
// 3. Seal the data.
// Only the ciphertext leaves this closure.
ciphertext = gcm.Seal(nonce, nonce, message, nil)
})
return ciphertext, encErr
}
This example highlights that secret.Do safeguards not just the raw cryptographic key, but also the cipher.Block structure, which contains the expanded key schedule, created within the protected function. It's important to remember that this is a simplified illustration of memory erasure, not a complete cryptographic exchange; a real-world scenario would also require secure key sharing with the receiver.