Skip to content

Timely mem::drop execution? #1850

@burdges

Description

@burdges

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions