Memory management
In the vast majority of cases, in non-unsafe Rust (i.e., code that does not use unsafe), the compiler automatically determines when it can release the memory occupied by a value in the program.
But, as noted earlier, this is not
a guarantee: code without unsafe can still lead to memory leaks. Therefore, some of the
rules presented in this chapter are not strictly related to the unsafe keyword. However,
even if a function in the following is not
unsafe, it should only be used in Rust unsafe.
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); }
Both functions are memory safe in Rust. However, forget will make any
resource managed by the value unreachable and unclaimed.
#![allow(unused)] fn main() { 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. This is why
forget is unsecure.
In a secure Rust development, the forget function of std::mem
(core::mem) must not be used.
The lint mem_forget of Clippy may be used to automatically detect any use of
forget. To enforce the absence of forget in a crate, add the following
line at the top of the root file (usually src/lib.rs or src/main.rs):
#![deny(clippy::mem_forget)]
The standard library includes other way to forget dropping values:
Box::leakto leak a resource,Box::into_rawto exploit the value in some unsafe code, notably in FFI,ManuallyDrop(instd::memorcore::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 (unsafe ManuallyDrop::drop).
Raw pointers
These pointers are mainly used for C pointers. They do not have the same protections
as smart pointers and often have to be used in unsafe context. For instance, freeing
raw pointers must be done manually without Rust guaranties.
In a secure Rust development without unsafe, references and smart pointers
should not be converted into raw pointers. For instance, functions into_raw ou into_non_null
of smart pointers Box, Rc, Arc or Weak should not be used.
In a secure Rust development, any pointer created with a call to into_raw
(or into_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 }
The converse is also true! That is, from_raw should be call only on into_rawed value. For instance,
Rc smart pointers explicitly request for this condition
and, for Box smart pointers, conversion of C pointers into Box is discouraged.
In a secure Rust development, from_raw should only be called on into_rawed values.
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::{Layout, dealloc}; 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 and Arc) 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 when using std::mem::uninitialized or
std::mem::MaybeUninit).
The std::mem::uninitialized function (deprecated 1.38) must never be used.
Each usage of the std::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.
std::mem::MaybeUninit is an improvement over std::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 remains. It is problematic, in particular when considering
the use of Drop to erase sensitive memory.
Cyclic reference counted pointers (Rc and Arc)
Combining interior mutability, recursivity and reference counted pointer into type definitions is unsafe. It can produce memory leaks which can result in DDoS attacks or leaking secrets.
The following example shows 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 uses reference counted pointers together with interior mutability.