-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
I've found that mem::drop
does not necessary run anyplace near where it gets called, which likely results in Mutex
or RwLock
guards being held during expensive computations.
As a simple example, I've made the following test for a zeroing drop of cryptographic material work by using unsafe { ::std::intrinsics::drop_in_place(&mut s); }
instead of ::std::mem::drop(s)
.
#[derive(Debug, Default)]
pub struct Secret<T>(pub T) where T: Copy;
impl<T> Drop for Secret<T> where T: Copy {
fn drop(&mut self) {
unsafe { ::std::intrinsics::volatile_set_memory::<Secret<T>>(self, 0, 1); }
}
}
#[derive(Debug, Default)]
pub struct AnotherSecret(pub [u8; 32]);
impl Drop for AnotherSecret {
fn drop(&mut self) {
unsafe { ::std::ptr::write_volatile::<AnotherSecret>(self, AnotherSecret([0u8; 32])); }
assert_eq!(self.0,[0u8; 32]);
}
}
#[cfg(test)]
mod tests {
// use crypto::digest::Digest;
// use crypto::sha3::Sha3;
macro_rules! zeroing_drop_test {
($n:path) => {
let p : *const $n;
{
let mut s = $n([3u8; 32]); p = &s;
// ::std::mem::drop(s);
unsafe { ::std::intrinsics::drop_in_place(&mut s); }
}
/*
let mut sha = Sha3::sha3_512();
let mut r = [0u8; 2*32];
for i in 0..10000 {
sha.input(&mut r);
sha.result(&mut r);
sha.reset();
}
*/
// ::std::thread::sleep(::std::time::Duration::from_secs(10));
unsafe { assert_eq!((*p).0,[0u8; 32]); }
}
}
#[test]
fn zeroing_drops() {
zeroing_drop_test!(super::Secret<[u8; 32]>);
zeroing_drop_test!(super::AnotherSecret);
}
}
This test fails if I use ::std::mem::drop(s)
or even
#[inline(never)]
pub fn drop_now<T>(_x: T) { }
At first, I imagined this was due to CPU pipelining, except this test still fails if I uncomment the sleep line or the 10k invocations of SHA3, or use an atomic_fense
, so the compiler looks like the guilty party.
There are two obvious scenarios where this seems problematic:
First, these zeroing drop calls should run eventually, but the longer the secret remains on the stack the greater the risk it gets compromised via side channel attacks, like being swapped to disk.
Second, we might run an expensive computation while holding a Mutex
or RwLock
guard that unlocks when dropped. We might even run code needing additional locks, thereby risking deadlocks.
In both case, one could fix the problem by calling drop_in_place
, but I'd think that creates use after free risks, although maybe it works if we do something fancier like
#[inline(never)]
pub fn drop_copy_now<T: Copy>(t: mut T) {
unsafe { ::std::intrinsics::volatile_set_memory::<Secret<T>>(&t, 0, 1); }
}
#[inline(never)]
pub fn drop_drop_now<T: Drop>(t: mut T) {
unsafe { ::std::intrinsics::drop_in_place(&mut t); }
unsafe { ::std::intrinsics::volatile_set_memory::<Secret<T>>(&t, 0, 1); }
}
Is there any safe way to ensure a drop happens before some expensive computation, system call, etc.? I suppose returning from fn
usually does so, but my drop_now
proves this sometimes fails.