Memory management
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.
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.
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.
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
).
Raw pointers
This pointers are mainly used to use C pointer. They do not have the same protections
than smart pointers and often have to be used in unsafe
context. For instance, freeing
raw pointer must be done manually without Rust warranties.
In a secure Rust development without
unsafe
, references and smart pointers should not be converted into raw pointers. For instance, functionsinto_raw
ouinto_non_null
of smart pointersBox
,Rc
,Arc
orWeak
should not be used.
In a secure Rust development, any pointer created with a call to
into_raw
(orinto_non_null
) 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 }
Converse is true! That is from_raw
should be call only on into_raw
ed value. For instance,
Rc
smart pointer explicitly request for this condition
and, for Box
smart pointer, conversion of C pointer into Box
is discouraged.
Règle MEM-INTOFROMRAW In a secure Rust development,
from_raw
should only be called oninto_raw
ed value
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
).
The
std::mem::uninitialized
function (deprecated 1.38) must never be used. Each usage of thestd::mem::MaybeUninit
type (stabilized 1.36) must be 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.
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 }
Cyclic reference counted pointers (Rc
and Arc
)
Combining interior mutability, recurcivity and reference counted pointer into type definitions is unsafe. It can produce memory leaks which can result in DDoS attack or secret leaks.
The following example show such a memory leak in safe Rust:
use std::{cell::Cell, rc::Rc}; struct LinkedStruct { other: Cell<Option<Rc<LinkedStruct>>>, } fn main() { println!("Hello, world!"); let a = Rc::new(LinkedStruct { other: Cell::new(None), }); let b = Rc::new(LinkedStruct { other: Cell::new(None), }); let aa = a.clone(); let bb = b.clone(); a.other.set(Some(bb)); b.other.set(Some(aa)); }
Memory leak is shown with valgrind
:
$ valgrind --leak-check=full target/release/safe-rust-leak
==153637== Memcheck, a memory error detector
==153637== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==153637== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==153637== Command: target/release/safe-rust-leak
==153637==
Hello, world!
==153637==
==153637== HEAP SUMMARY:
==153637== in use at exit: 48 bytes in 2 blocks
==153637== total heap usage: 10 allocs, 8 frees, 3,144 bytes allocated
==153637==
==153637== 48 (24 direct, 24 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2
==153637== at 0x48417B4: malloc (vg_replace_malloc.c:381)
==153637== by 0x10F8D4: safe_rust_leak::main (in /home/toto/src/safe-rust-leak/target/release/safe-rust-leak)
==153637== by 0x10F7E2: std::sys::backtrace::__rust_begin_short_backtrace (in /home/toto/src/safe-rust-leak/target/release/safe-rust-leak)
==153637== by 0x10F7D8: std::rt::lang_start::{{closure}} (in /home/toto/src/safe-rust-leak/target/release/safe-rust-leak)
==153637== by 0x12A90F: std::rt::lang_start_internal (in /home/toto/src/safe-rust-leak/target/release/safe-rust-leak)
==153637== by 0x10FA54: main (in /home/toto/src/safe-rust-leak/target/release/safe-rust-leak)
==153637==
==153637== LEAK SUMMARY:
==153637== definitely lost: 24 bytes in 1 blocks
==153637== indirectly lost: 24 bytes in 1 blocks
==153637== possibly lost: 0 bytes in 0 blocks
==153637== still reachable: 0 bytes in 0 blocks
==153637== suppressed: 0 bytes in 0 blocks
==153637==
==153637== For lists of detected and suppressed errors, rerun with: -s
==153637== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Avoid recursive types whose recursivity use reference counted pointers together with interior mutability.