TODO: recommendations about correctly naming things (RFC 430).
TODO: explain safe allocations/deallocations, ownership/borrowing, and identify language constructs that may break memory safety (for instance, unsound behaviors in older versions of the compiler).
By default, Rust forces all values to be initialized, preventing the use of
uninitialized memory (except if using std::mem::uninitialized).
However, zeroing memory is useful for sensitive variables, especially if the
Rust code is used through FFI.
Variables containing sensitive data must be zeroized after use, using functions that will not be removed by the compiler optimizations, like
std::ptr::write_volatileor thezeroizecrate.The
std::mem::uninitializedfunction must not be used, or explicitly justified when necessary.
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
# }The joint utilization of the type system and the ownership system aims to
enforce safety regarding the memory management in Rust's programs. So the
language aims to avoid memory overflows, null or invalid pointer constructions,
and data races.
To perform risky actions such as system calls, type coercions, or memory
pointers direct manipulations, the language provides the unsafe keyword.
For a secured development, the
unsafeblocks must be avoided. Afterward, we list the only cases whereunsafemay be used, provided that they come with a proper justification:
The Foreign Function Interface (FFI) of Rust allows for describing functions whose implementation is written in C, using the
extern "C"prefix. To use such a function, theunsafekeyword is required. “Safe” wrapper shall be defined to safely and seamlessly call C code.For embedded device programming, registers and various other resources are often accessed through a fixed memory address. In this case,
unsafeblocks are required to initialize and dereference those particular pointers in Rust. In order to minimize the number of unsafe accesses in the code and to allow easier identification of them by a programmer, a proper abstraction (data structure or module) shall be provided.A function can be marked unsafe globally (by prefixing its declaration with the
unsafekeyword) when it may exhibit unsafe behaviors based on its arguments, that are unavoidable. For instance, this happens when a function tries to dereference a pointer passed as an argument.With the exception of these cases,
#[forbid(unsafe_code)]must appear inmain.rsto generate compilation errors ifunsafeis used in the code base.
Although some verification is performed by Rust regarding the potential integer overflows, precautions should be taken when executing arithmetic operations on integers.
In particular, it should be noted that using debug or release compilation
profile changes integer overflow behavior. In debug configuration, overflow
cause the termination of the program (panic), whereas in the release configuration
the computed value silently wraps around the maximum value that can be stored.
This last behavior can be made explicit by using the Wrapping generic type,
or the overflowing_<op> and wrapping_<op> operations on integers
(the <op> part being add, mul, sub, shr, etc.).
use std::num::Wrapping;
# use std::panic;
# fn main() {
let x: u8 = 242;
# let result = panic::catch_unwind(|| {
println!("{}", x + 50); // panics in debug, prints 36 in release.
# });
# if result.is_err() { println!("panic"); }
println!("{}", x.overflowing_add(50).0); // always prints 36.
println!("{}", x.wrapping_add(50)); // always prints 36.
println!("{}", Wrapping(x) + Wrapping(50)); // always prints 36.
// always panics:
let (res, c) = x.overflowing_add(50);
# let result = panic::catch_unwind(|| {
if c { panic!("custom error"); }
else { println!("{}", res); }
# });
# if result.is_err() { println!("panic"); }
# }When assuming that an arithmetic operation can produce an overflow, the specialized functions
overflowing_<op>,wrapping_<op>, or theWrappingtype must be used.
TODO: identify pitfalls with the type system (for instance, misunderstanding of which code is actually executed when implementing complex patterns with traits).
TODO: explicit good practices in error handling.
The Result type is the preferred way of handling functions that can fail.
A Result object must be tested, and never ignored.
A crate can implement its own Error type, wrapping all possible errors. It must be careful to make this type exception-safe (RFC 1236), and implement
Error + Send + Sync + 'staticas well asDisplay.
The
?operator should be used to improve readability of code. Thetry!macro should not be used.
The error-chain and failure crates can be used to wrap errors.
Explicit error handling (Result) should always be preferred instead of calling
panic. The cause of the error should be available, and generic errors should
be avoided.
Crates providing libraries should never use functions or instructions that can fail and cause the code to panic.
Common patterns that can cause panics are:
- using
unwraporexpect - using
assert - an unchecked access to an array
- integer overflow (in debug mode)
- division by zero
- large allocations
- string formatting using
format!
Functions or instructions that can cause the code to panic at runtime must not be used.
Array indexing must be properly tested, or the
getmethod should be used to return anOption.
TODO Check if the no_panic
crate can catch all cases. Drawback: all functions need to be marked as
#[no_panic].
When calling Rust code from another language (for ex. C), the Rust code must be careful to never panic. Unwinding from Rust code into foreign code results in undefined behavior.
Rust code called from FFI must either ensure the function cannot panic, or use
catch_unwindor thestd::panicmodule to ensure the rust code will not abort or return in an unstable state.
Note that catch_unwind will only catch unwinding panics, not those that abort
the process.
-
Drop TODO
-
The
SendandSynctraits are marker traits that allow implementors to specify respectively that a value can be sent (moved) to another thread and that a value be shared through a shared reference. They both are unsafe traits, that should be used. Moreover, theunsafekeyword is necessary for a type to implement those traits. -
PartialOrd, Ord, Eq TODO
TODO: cyclomatic complexity of the macro expanded code, recursion limits, ...