Memory management
Forget and memory leaks
While the usual way for memory to be reclaimed is for a variable to go out of
scope, Rust provides special functions to manually reclaim memory: forget
and
drop
of the std::mem
module (or core::mem
). While drop
simply triggers
an early memory reclamation that calls associated destructors when needed,
forget
skips any call to the destructors.
#![allow(unused)] fn main() { let pair = ('↑', 0xBADD_CAFEu32); drop(pair); // here `forget` would be equivalent (no destructor to call) }
Both functions are memory safe in Rust. However, forget
will make any
resource managed by the value unreachable and unclaimed.
#![allow(unused)] fn main() { use std::mem::forget; let s = String::from("Hello"); forget(s); // Leak memory }
In particular, using forget
may result in not releasing critical resources
leading to deadlocks or not erasing sensitive data from the memory. That is why,
forget
is unsecure.
Rule MEM-FORGET
In a secure Rust development, the
forget
function ofstd::mem
(core::mem
) must not be used.
Recommendation MEM-FORGET-LINT
The lint
mem_forget
of Clippy may be used to automatically detect any use offorget
. To enforce the absence offorget
in a crate, add the following line at the top of the root file (usuallysrc/lib.rs
orsrc/main.rs
):#![deny(clippy::mem_forget)]
The standard library includes other way to forget dropping values:
Box::leak
to leak a resource,Box::into_raw
to exploit the value in some unsafe code, notably in FFI,ManuallyDrop
(instd::mem
orcore::mem
) to enforce manual release of some value.
Those alternatives may lead to the same security issue but they have the additional benefit of making their goal obvious.
Rule MEM-LEAK
In a secure Rust development, the code must not leak memory or resource in particular via
Box::leak
.
ManuallyDrop
and Box::into_raw
shift the release responsibility from the
compiler to the developer.
Rule MEM-MANUALLYDROP
In a secure Rust development, any value wrapped in
ManuallyDrop
must be unwrapped to allow for automatic release (ManuallyDrop::into_inner
) or manually released (unsafeManuallyDrop::drop
).
Rule MEM-INTOFROMRAW
In a secure Rust development, any pointer created with a call to
into_raw
(orinto_raw_nonnull
) from one of the following types:
std::boxed::Box
(oralloc::boxed::Box
),std::rc::Rc
(oralloc::rc::Rc
),std::rc::Weak
(oralloc::rc::Weak
),std::sync::Arc
(oralloc::sync::Arc
),std::sync::Weak
(oralloc::sync::Weak
),std::ffi::CString
,std::ffi::OsString
,must eventually be transformed into a value with a call to the respective
from_raw
to allow for their reclamation.#![allow(unused)] fn main() { let boxed = Box::new(String::from("Crab")); let raw_ptr = unsafe { Box::into_raw(boxed) }; let _ = unsafe { Box::from_raw(raw_ptr) }; // will be freed }
Note
In the case of
Box::into_raw
, manual cleanup is possible but a lot more complicated than re-boxing the raw pointer and should be avoided:#![allow(unused)] fn main() { // Excerpt from the standard library documentation use std::alloc::{dealloc, Layout}; use std::ptr; let x = Box::new(String::from("Hello")); let p = Box::into_raw(x); unsafe { ptr::drop_in_place(p); dealloc(p as *mut u8, Layout::new::<String>()); } }
Because the other types (
Rc
andArc
) are opaque and more complex, manual cleanup is not possible.
Uninitialized memory
By default, Rust forces all values to be initialized, preventing the use of
uninitialized memory (except if using std::mem::uninitialized
or
std::mem::MaybeUninit
).
Rule MEM-UNINIT
The
std::mem::uninitialized
function (deprecated 1.38) or thestd::mem::MaybeUninit
type (stabilized 1.36) must not be used, or explicitly justified when necessary.
The use of uninitialized memory may result in two distinct security issues:
- drop of uninitialized memory (also a memory safety issue),
- non-drop of initialized memory.
Note
std::mem::MaybeUninit
is an improvement overstd::mem::uninitialized
. Indeed, it makes dropping uninitialized values a lot more difficult. However, it does not change the second issue: the non-drop of an initialized memory is as much likely. It is problematic, in particular when considering the use ofDrop
to erase sensitive memory.
Secure memory zeroing for sensitive information
Zeroing memory is useful for sensitive variables, especially if the Rust code is used through FFI.
Rule MEM-ZERO
Variables containing sensitive data must be zeroed out after use, using functions that will not be removed by the compiler optimizations, like
std::ptr::write_volatile
or thezeroize
crate.
The following code shows how to define an integer type that will be set to
0 when freed, using the Drop
trait:
/// Example: u32 newtype, set to 0 when freed pub struct ZU32(pub u32); impl Drop for ZU32 { fn drop(&mut self) { println!("zeroing memory"); unsafe{ ::std::ptr::write_volatile(&mut self.0, 0) }; } } fn main() { { let i = ZU32(42); // ... } // i is freed here }