Skip to content

Latest commit

 

History

History
218 lines (169 loc) · 7.66 KB

File metadata and controls

218 lines (169 loc) · 7.66 KB

The Rust language

Generalities

Naming

TODO: recommendations about correctly naming things (RFC 430).

Memory management

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.

Rule [MEM-ZERO]:

Variables containing sensitive data must be zeroized after use, using functions that will not be removed by the compiler optimizations, like std::ptr::write_volatile or the zeroize crate.

The std::mem::uninitialized function 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
# }

Unsafe code

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.

Rule [LANG-UNSAFE]:

For a secured development, the unsafe blocks must be avoided. Afterward, we list the only cases where unsafe may 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, the unsafe keyword 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, unsafe blocks 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 unsafe keyword) 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 in main.rs to generate compilation errors if unsafe is used in the code base.

Integer overflows

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"); }
# }

Rule [LANG-ARITH]:

When assuming that an arithmetic operation can produce an overflow, the specialized functions overflowing_<op>, wrapping_<op>, or the Wrapping type must be used.

Type system

TODO: identify pitfalls with the type system (for instance, misunderstanding of which code is actually executed when implementing complex patterns with traits).

Error handling

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.

Recommendation [LANG-ERRWRAP]:

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 + 'static as well as Display.

Recommendation [LANG-ERRDO]:

The ? operator should be used to improve readability of code. The try! macro should not be used.

The error-chain and failure crates can be used to wrap errors.

Panics

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 unwrap or expect
  • using assert
  • an unchecked access to an array
  • integer overflow (in debug mode)
  • division by zero
  • large allocations
  • string formatting using format!

Rule [LANG-NOPANIC]:

Functions or instructions that can cause the code to panic at runtime must not be used.

Rule [LANG-ARRINDEXING]:

Array indexing must be properly tested, or the get method should be used to return an Option.

TODO Check if the no_panic crate can catch all cases. Drawback: all functions need to be marked as #[no_panic].

FFI and panics

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.

Rule [LANG-FFIPANIC]:

Rust code called from FFI must either ensure the function cannot panic, or use catch_unwind or the std::panic module 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.

Standard library traits

  • Drop TODO

  • The Send and Sync traits 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, the unsafe keyword is necessary for a type to implement those traits.

  • PartialOrd, Ord, Eq TODO

Macros

TODO: cyclomatic complexity of the macro expanded code, recursion limits, ...