-
Notifications
You must be signed in to change notification settings - Fork 18.6k
Description
Proposal Details
With looser guarantees coming, it would be helpful if users of secret could designate allocations that shouldn't be zeroed. For example:
var ciphertext []byte
secret.Do(func() {
// set up keys
// <some code...>
ciphertext = gcm.Seal(nil, nonce, plaintext, nil)
})
// stores ciphertext it somewhereWith the new guarantees, any allocations that gcm.Seal does while calculating ciphertext are not guaranteed to be erased until ciphertext is deemed unreachable.
Right now, the new guarantees are just for handling corner cases in some optimizations(see #76764). Generally, we treat a group of allocations done inside a secret block as distinct individual allocations that get zeroed independently of each other and GC overhead is the only thing stopping us from writing the above code. However, the new guarantees allow us to be much more agressive in our optimizations. For example, using a specific span class for eager zeroization, which would fix the double erasing issue. Beyond that, allowing heap memory to be returned facilitates better API design.
proposal
I propose that we add the Exempt function to the runtime/secret package
// Exempt marks an allocation as being exempt from being erased
// after a call to Do. The underlying object pointed to by obj must not be
// referenced after a call to Exempt
//
// Calls to Exempt only mark the argument given as being exempt. Any allocations
// pointed to by the object are still subject to the Do guarantees
func Exempt[E any](obj E) EAn example of use:
var ciphertext []byte
secret.Do(func() {
//...
ciphertext = gcm.Seal(nil, nonce, plaintext, nil)
ciphertext = secret.Exempt(ciphertext)
})Rationale
Having Exempt return a new reference means that we can implement it as a copy. This will be helpful if we decide to implement separate allocator spans for secret code.
Implementation
As it is currently, the implementation would be clearing the special allocation from the objects span. The locking overhead might be an issue, in which case, we could implement it as a copy.
Alternatives
Wrapper function
An ignore function like discussed for regions would be overly broad. Going back to our running example:
var ciphertext []byte
secret.Do(func() {
//...
secret.Ignore(func() {
ciphertext = gcm.Seal(nil, nonce, plaintext, nil)
}
})in this case, the allocations exempted would cover everything that gcm.Seal might do, leaving potentially confidential info in the heap. It is also hard to reason about. Only the guarantees related to the heap are changed, but a user could easily get confused, thinking that the registers and stack clearing is also made ineffectual.
Doing nothing
Cryptographic libraries are, in general, designed such that they can be used without allocating. Our running example again:
ciphertext := make([]byte, 0, len(plaintext) + overhead)
secret.Do(func() {
//...
ciphertext = gcm.Seal(ciphertext, nonce, plaintext, nil)
})However, this API design is not universal.
There is also another related alternative that can be used even when the API isn't designed for preventing allocations:
ciphertext := make([]byte, 0, len(plaintext))
secret.Do(func() {
//...
// pretend that gcm.Seal does not that a destination slice
secretciphertext := gcm.Seal(nil, nonce, plaintext, nil)
copy(ciphertext, secretciphertext)
})This would instead force us to do a copy. The Exempt function signature lets us implement the carve-out whichever way is the most convenient for what the runtime is doing right now.
To be determined
- the type parameter is deliberately unrestrictive right now. Is there a more restrictive set of types that would still be useful for users, but reduces the state space we'd have to deal with in the future. How would
Exemptinteract with a channel or a map?