The idea is quite simple, let's add two special derivable marker traits ZeroizeMoveSrc
and ZeroizeMoveDst
(names are up to bikeshedding), which will slightly modify the "move is a simple memcpy
" rule.
If ZeroizeMoveSrc
is implemented for a type, then immediately after memcpy
source bytes on stack will be overwritten by zeros using volatile write. After value of that type goes out of scope or after its drop
ped, previous location of that value gets zeroized as well (i.e. we can view those operations as moving data into nothing). If type implements this trait and contains another type which also implements it (Keys
type in the next example), then zeroization will be done only ones.
#[derive(ZeroizeMoveSrc)]
struct EncryptionKey([u8; 16]);
#[derive(ZeroizeMoveSrc)]
struct Keys { enc_key: EncryptionKey, mac_key: [u8; 16] }
Note that only bytes on stack will be zeroed out, so data on heap behind b
will not be erased for the following type:
#[derive(ZeroizeMoveSrc)]
struct Foo { a: [u8; 16], b: Box<[u8]> }
But if data is moved from heap, then original location on heap will be zeroized.
The main use-case for this trait is handling of a sensitive secret data (e.g. cryptographic keys). We already have a number of crates which target this problem (e.g. zeroize
) via a specialized Drop
implementation, but they have several problems. The most important one is that they can't deal with moves at all. Another one is related to performance: with the Keys
type zeroization on drop will be done twice, which may hurt performance.
ZeroizeMoveDst
will zeroize destination memory before moving data into it (write can be non-volatile). This will done before creation of a value implementing this trait as well. Similarly to the ZeroizeMoveSrc
trait, zeroization of destination memory will not be duplicated if type contains another type which implements ZeroizeMoveDst
. The main use-case for this trait is safe transmute of a value into raw bytes using function like this:
use core::mem::size_of;
fn into_raw_bytes<T: ZeroizeMoveDst>(val: &T) -> &[u8; size_of::<T>()] {
unsafe { &*(val.as_ptr() as *const _) }
}
This approach will allow us to work around the problem of undefined padding bytes without debating semantics of freeze
and with only minor performance impact (since zeroization of destination memory is not volatile it can be removed if not observed).
Relevant links: