How to check that an object is zeroized on drop

I am using k256 library, and want to make sure that the contents of SecretKey are zeroized on drop. I wrote a small test, but I am confused by the results.

// k256 = { version = "0.9.2", default-features = false, features = ["ecdsa", "zeroize"] }
use k256::SecretKey;
// rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
use rand_core::OsRng;

fn drop_sk() -> usize {
    let sk = SecretKey::random(&mut OsRng);
    let ptr = &sk as *const SecretKey;
    let ptr_u8 = ptr as *const u8;

    // Line 1
    // println!("Memory in drop_sk() #1: {:?}", unsafe { core::slice::from_raw_parts(ptr_u8, 4) });
    // Line 2
    // drop(sk);
    // Line 3
    // println!("Memory in drop_sk() #2: {:?}", unsafe { core::slice::from_raw_parts(ptr_u8, 4) });

    ptr_u8 as usize
}

fn main() {
    let ptr_u8 = drop_sk();

    println!("Memory in main(): {:?}", unsafe { core::slice::from_raw_parts(ptr_u8 as *const u8, 4) });
}

Now, before running it, I expected the output in main() always be the same regardless of which of Lines 1-3 are commented or not in drop_sk(), because either way it is dropped at the end of it, and should be zeroized. But the results actually differ:

# all commented
Memory in main(): [1, 0, 0, 0]

# only line 1 uncommented (or only line 3 uncommented)
Memory in drop_sk() #1: [122, 34, 83, 67]
Memory in main(): [0, 0, 0, 0]
# (so, zeroized as expected)

# only line 2 uncommented
Memory in main(): [2, 0, 0, 0]

# lines 1 and 2 uncommented (or lines 2 and 3 uncommented)
Memory in drop_sk() #1: [95, 249, 139, 251]
Memory in main(): [95, 249, 139, 251]
# (no zeroization)

# lines 1 and 3 uncommented
Memory in drop_sk() #1: [134, 166, 37, 46]
Memory in drop_sk() #2: [134, 166, 37, 46]
Memory in main(): [3, 0, 0, 0]

# all lines uncommented
Memory in drop_sk() #1: [212, 233, 107, 78]
Memory in drop_sk() #2: [212, 233, 107, 78]
Memory in main(): [0, 0, 0, 0] 

For reference, the structure of SecretKey is SecretKey { inner: ScalarBytes { inner: GenericArray<...> } }, where neither SecretKey nor ScalarBytes have Copy implemented.

I do not know how to interpret these results. Somehow an explicit drop() call leaves the secret data in memory, but if you print it again, the zeroization happens. Does explicit drop() and automatic going-out-of-scope drop work differently? Does taking a pointer on an object somehow prevents it from being dropped?

There are no guarantees about the contents of former locations when a value is moved. Even the call to drop means the value will be moved into that function call and dropped there, but that says nothing about the contents of its former stack memory. There's some discussion of that here:

https://docs.rs/zeroize/1.3.0/zeroize/#stackheap-zeroing-notes

Semantically, your examples will call the zeroing Drop when drop_sk or std::mem::drop returns, depending whether you've uncommented that call. But again that says nothing about the moved-from memory locations, especially once inlining and further optimization gets involved.

1 Like

I see, so it's the stack allocation that causes all this. Is there a best practice for these cases, some way to 100% ensure that the object is not copied and will be zeroized when it goes out of scope? Would wrapping SecretKey in a Box and pinning it help?

Edit: apparently there will still be at least one stack copy when SecretKey is created before it is being put in a box; explicit placement is still unstable. So I guess there's that, no 100% guarantee.

I couldn't find a way to test zeroization either. I tried doing the following:

    let sk = Box::new(SecretKey::random(&mut OsRng));
    let ptr = Box::<SecretKey>::into_raw(sk);
    let ptr_u8 = ptr as usize;

    unsafe {
        let sk_back = Box::<SecretKey>::from_raw(ptr);
    }

but the secret data at ptr is still not cleared after sk_back is dropped in the unsafe scope.

Edit 2:

Even the call to drop means the value will be moved into that function call and dropped there, but that says nothing about the contents of its former stack memory.

If the value does not implement Copy, shouldn't the inner function get a pointer to the stack value of the outer function (internally)?

You might be able to use Pin to keep from accidentally moving, as those docs suggest, but even then there are a lot of ways a value might escape. Any function that computes with it could load into registers and spill onto the stack, for example. So Zeroize is an okay step of due diligence, but it's nowhere near complete secrecy.

No, the value can still be moved. Copy and moves are both essentially implemented with memcpy, but non-Copy types aren't allowed to read the original location after they've moved.

1 Like

Using Pin does not help beyond what you get from using a Box. We discussed that here yesterday.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.