Making Unsafe Rust a Little Safer: Tools for Verifying Unsafe Code, Including Libraries in C and C++
One reason Rust has become an increasingly popular systems programming language is it affords excellent performance while eliminating memory and concurrency errors at compile time, errors that are hard to avoid in other languages with similar performance properties, like C and C++. However, it is possible to bypass these compile-time checks by writing unsafe Rust. While the vast majority of programmers should never write unsafe Rust, some libraries use unsafe Rust for performance, direct manipulation of memory or hardware, or for integration with other libraries and system calls.[1] This article will look at tools for verifying unsafe Rust code, including unsafe code called from libraries written in C or C++. My interest in exploring this topic is writing safe and reliable software for operational technologies (OT) and critical infrastructure.
Sanitizers
Sanitizers are tools that detect various programming errors, like memory corruption, memory leaks, or data races across threads, at run-time. They work by instrumenting the code during compilation to insert checks that validate program behaviour. Sanitizers introduce memory and performance overhead and are generally used in test environments. Importantly, unlike a compiler, a sanitizer will only detect errors in code paths that are actually exercised at run-time, either through tests or by running the program itself.
When I first read that Rust supports sanitizers for finding errors, I was surprised. I’m familiar with using sanitizers in C and C++ through Clang and the LLVM compiler infrastructure. However, because the Rust compiler, rustc
, uses the LLVM compiler infrastructure, it can take advantage of the same sanitizers.
Out-of-Bounds Memory Access
Consider the following program:[2]
fn bad_address(i: i32) -> i32 {
let xs: [i32; 4] = [0, 1, 2, 3];
xs[i as usize]
}
fn main() {
let v = bad_address(4);
println!("Value at offset: {}", v);
}
When I run the program with RUST_BACKTRACE=1 cargo run --release
, Rust’s bounds checking detects the error and the program panics:
thread 'main' panicked at src/main.rs:3:5:
index out of bounds: the len is 4 but the index is 4
stack backtrace:
0: _rust_begin_unwind
1: core::panicking::panic_fmt
2: core::panicking::panic_bounds_check
3: sanitizers::main
The program terminates, which may be highly undesirable, or even unacceptable, creating other safety issues if the software is vital for operating critical infrastructure, but the run-time checks ensure the program will never execute unsafe code that would result in undefined behaviour.[3]
Now consider what happens if the function uses an unsafe code block to index into the array using a pointer:[4]
fn bad_address(i: i32) -> i32 {
let xs: [i32; 4] = [0, 1, 2, 3];
unsafe { *xs.as_ptr().offset(i as isize) }
}
fn main() {
let v = bad_address(4);
println!("Value at offset: {}", v);
}
With unsafe code, the Rust compiler no longer provides assurances for memory and thread safety. It is the programmer’s responsibility to ensure the unsafe code follows the rules and is free of unsafe or undefined behaviours. When I run this code, it does not panic even though it reads memory outside the bounds of the array:[5]
Value at offset: 24576
The Rust AddressSanitizer can detect out-of-bounds access on the stack and heap.[6] It does this by inserting red-zones between memory allocations and tracking if they have been illegally read or written using shadow memory.[7] The sanitizers require Rust’s nightly toolchain, rather than the stable toolchain, but they are easy to run side-by-side. To install the nightly toolchain:
rustup install nightly
Then run the program with the AddressSanitizer enabled:
export RUSTFLAGS=-Zsanitizer=address
cargo +nightly run
The program will crash with a detailed error report for the out-of-bounds access:
================================================================= ==96148==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016dce67b0 at pc 0x00010211bf70 bp 0x00016dce6770 sp 0x00016dce6768 READ of size 4 at 0x00016dce67b0 thread T0 #0 0x00010211bf6c in array_out_of_bounds_unsafe::bad_address::h9a9dae85f9ad5feb array_out_of_bounds_unsafe.rs:3 #1 0x00010211c170 in array_out_of_bounds_unsafe::main::hc84cbff8319e0a2b array_out_of_bounds_unsafe.rs:7 #2 0x00010211bd40 in core::ops::function::FnOnce::call_once::hc75a52fb9134d583 function.rs:250 #3 0x00010211bd8c in std::sys::backtrace::__rust_begin_short_backtrace::h9c09c1d17c8393c3 backtrace.rs:152 #4 0x00010211b888 in std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h3a3a442dfff79e34 rt.rs:195 #5 0x000102135230 in std::rt::lang_start_internal::hc996363c321dd410+0x440 (array_out_of_bounds_unsafe:arm64+0x10001d230) #6 0x00010211b6c0 in std::rt::lang_start::hae3ff67dcefd99eb rt.rs:194 #7 0x00010211c2e0 in main+0x20 (array_out_of_bounds_unsafe:arm64+0x1000042e0) #8 0x00019d87e0dc () #9 0xf4687ffffffffffc ( ) Address 0x00016dce67b0 is located in stack of thread T0 at offset 48 in frame #0 0x00010211bdbc in array_out_of_bounds_unsafe::bad_address::h9a9dae85f9ad5feb array_out_of_bounds_unsafe.rs:1 This frame has 1 object(s): [32, 48) 'xs' (line 2) <== Memory access at offset 48 overflows this variable HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions *are* supported) SUMMARY: AddressSanitizer: stack-buffer-overflow array_out_of_bounds_unsafe.rs:3 in array_out_of_bounds_unsafe::bad_address::h9a9dae85f9ad5feb Shadow bytes around the buggy address: 0x00016dce6500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016dce6580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016dce6600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016dce6680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016dce6700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x00016dce6780: f1 f1 f1 f1 00 00[f3]f3 00 00 00 00 00 00 00 00 0x00016dce6800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016dce6880: 00 00 00 00 f1 f1 f1 f1 f8 f8 f2 f2 f8 f8 f8 f8 0x00016dce6900: f8 f8 f2 f2 f2 f2 04 f3 00 00 00 00 00 00 00 00 0x00016dce6980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016dce6a00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==96148==ABORTING
The example above was run in debug. If run in release it won’t necessarily identify the error due to compiler optimizations. When using sanitizers with a release build, be sure to disable compiler optimizations:
export RUSTFLAGS="-C opt-level=0 -Zsanitizer=address"
cargo +nightly run --release
Furthermore, the AddressSanitizer will not always identify the out-of-bounds error. I can get the program above to run without error, fail with a SEGV on an unknown address, or fail with a stack overflow, depending on the index I use into the array.
Data Race
To round out this section on sanitizers, I will explore one more example of an error in unsafe Rust code that can be detected with a sanitizer. Consider the following code that accesses a shared, mutable variable from unsafe code in different threads:
fn main() {
static mut A: usize = 0;
let t = std::thread::spawn(|| {
unsafe { A += 1 };
});
unsafe { A += 1 };
t.join().unwrap();
}
Running this program normally will not result in run-time errors, but when run with the ThreadSanitizer enabled:
export RUSTFLAGS=-Zsanitizer=thread
cargo +nightly run
It will detect the data race and produce a detailed report:
================== WARNING: ThreadSanitizer: data race (pid=12331) Read of size 8 at 0x000104f40460 by thread T1: #0 sanitizers::main::_$u7b$$u7b$closure$u7d$$u7d$::h77c6a8d926b4ffd9 main.rs:5 (sanitizers:arm64+0x10000ae68) #1 std::sys::backtrace::__rust_begin_short_backtrace::h3d7723e74dc43907 backtrace.rs:152 (sanitizers:arm64+0x100008f6c) #2 std::thread::Builder::spawn_unchecked_::_$u7b$$u7b$closure$u7d$$u7d$::_$u7b$$u7b$closure$u7d$$u7d$::h972cca723fc4d46a mod.rs:561 (sanitizers:arm64+0x1000033a4) #3 _$LT$core..panic..unwind_safe..AssertUnwindSafe$LT$F$GT$$u20$as$u20$core..ops..function..FnOnce$LT$$LP$$RP$$GT$$GT$::call_once::h339263acc4287c3b unwind_safe.rs:272 (sanitizers:arm64+0x100004e64) #4 std::panicking::try::do_call::h1720a438c6154692 panicking.rs:573 (sanitizers:arm64+0x1000090b8) #5 __rust_try(sanitizers:arm64+0x10000351c) #6 std::thread::Builder::spawn_unchecked_::_$u7b$$u7b$closure$u7d$$u7d$::h4a9e1cb91e992611 mod.rs:559 (sanitizers:arm64+0x100002cf0) #7 core::ops::function::FnOnce::call_once$u7b$$u7b$vtable.shim$u7d$$u7d$::h4d54278169623269 function.rs:250 (sanitizers:arm64+0x100005254) #8 std::sys::pal::unix::thread::Thread::new::thread_start::h5efa5b2bb0838bc2 (sanitizers:arm64+0x10002acd4) Previous write of size 8 at 0x000104f40460 by main thread: #0 sanitizers::main::he9b6ca8696085c08 main.rs:7 (sanitizers:arm64+0x100004b9c) #1 core::ops::function::FnOnce::call_once::hba41c0d640901898 function.rs:250 (sanitizers:arm64+0x10000540c) #2 std::sys::backtrace::__rust_begin_short_backtrace::hc59209a0a1d24814 backtrace.rs:152 (sanitizers:arm64+0x10000901c) #3 std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h915b5f69c928813d rt.rs:195 (sanitizers:arm64+0x100005148) #4 std::rt::lang_start_internal::hc996363c321dd410 (sanitizers:arm64+0x10002428c) #5 main (sanitizers:arm64+0x100004d6c) Location is global 'sanitizers::main::A::h92ea287e34ba2e52' at 0x000104f40460 (sanitizers+0x100058460) Thread T1 (tid=11227209, running) created by main thread at: #0 pthread_create (librustc-nightly_rt.tsan.dylib:arm64+0xa0a8) #1 std::sys::pal::unix::thread::Thread::new::h0b16ad3e3a52b1cf (sanitizers:arm64+0x10002ab38) #2 std::thread::Builder::spawn_unchecked::hbd40c84e3aa877bf mod.rs:467 (sanitizers:arm64+0x100002028) #3 std::thread::spawn::hd17317d53012bcc4 mod.rs:730 (sanitizers:arm64+0x100001fa0) #4 sanitizers::main::he9b6ca8696085c08 main.rs:4 (sanitizers:arm64+0x100004b54) #5 core::ops::function::FnOnce::call_once::hba41c0d640901898 function.rs:250 (sanitizers:arm64+0x10000540c) #6 std::sys::backtrace::__rust_begin_short_backtrace::hc59209a0a1d24814 backtrace.rs:152 (sanitizers:arm64+0x10000901c) #7 std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h915b5f69c928813d rt.rs:195 (sanitizers:arm64+0x100005148) #8 std::rt::lang_start_internal::hc996363c321dd410 (sanitizers:arm64+0x10002428c) #9 main (sanitizers:arm64+0x100004d6c) SUMMARY: ThreadSanitizer: data race main.rs:5 in sanitizers::main::_$u7b$$u7b$closure$u7d$$u7d$::h77c6a8d926b4ffd9 ================== ThreadSanitizer: reported 1 warnings
Miri
The suite of sanitizers Rust supports is invaluable for finding errors in unsafe code, but they are not completely deterministic—they will not find all errors. In addition, not all of the sanitizers are compatible, and running them independently increases the number of tests and the time invested in testing.
Miri is an interpreter that can deterministically find undefined behaviours in unsafe code, including out-of-bound access, memory leaks, use of uninitialized data, use after free, data races, and more. Miri works by deterministically interpreting Rust’s Mid-Level Intermediate Representation (also known as Mid-Level IR, or MIR for short) so it is halfway between the static analysis of the compiler and the dynamic analysis of running code with sanitizers.
Like the sanitizers, Miri also relies on Rust’s nightly toolchain and is simple to install:
rustup +nightly component add miri
Out-of-Bounds Memory Access
Consider the out-of-bounds memory access from above:
fn bad_address(i: i32) -> i32 {
let xs: [i32; 4] = [0, 1, 2, 3];
unsafe { *xs.as_ptr().offset(i as isize) }
}
fn main() {
let v = bad_address(4000);
println!("Value at offset: {}", v);
}
Using Miri is straightforward:
cargo +nightly miri run
Miri will report the out-of-bounds access along with a back-trace:
error: Undefined Behavior: out-of-bounds pointer arithmetic: expected a pointer to 16000 bytes of memory, but got alloc870 which is only 16 bytes from the end of the allocation --> src/main.rs:3:15 | 3 | unsafe { *xs.as_ptr().offset(i as isize) } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ out-of-bounds pointer arithmetic: expected a pointer to 16000 bytes of memory, but got alloc870 which is only 16 bytes from the end of the allocation | = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information help: alloc870 was allocated here: --> src/main.rs:2:9 | 2 | let xs: [i32; 4] = [0, 1, 2, 3]; | ^^ = note: BACKTRACE (of the first span): = note: inside `bad_address` at src/main.rs:3:15: 3:45 note: inside `main` --> src/main.rs:7:13 | 7 | let v = bad_address(4000); | ^^^^^^^^^^^^^^^^^ note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace error: aborting due to 1 previous error
While the sanitizer detected the same error, Miri’s output is more specific and easier to interpret, including code snippets rather than memory addresses and stack frames. Miri is also checking for many types of undefined behaviour in a single execution. Similar to the sanitizers, Miri only interprets the code paths that are executed, either in tests or in a binary, and it will not find errors in code paths that are not interpreted. For a deeper understanding of how Miri works and the errors it is capable of detecting, I recommend the talk Unsafe Rust and Miri by Ralf Jung.
Data Race
For completeness, I will return to the code with the data race, but this time I will call it using Miri from a test:
fn data_race() {
static mut A: usize = 0;
let t = std::thread::spawn(|| {
unsafe { A += 1 };
});
unsafe { A += 1 };
t.join().unwrap();
}
#[cfg(test)]
mod tests {
use crate::data_race;
#[test]
fn data_race_test() {
data_race();
}
}
To run Miri on tests:
cargo +nightly miri test
Miri successfully identifies the data race and includes specific code snippets and errors that are much easer to interpret than the output from the Rust ThreadSanitizer example above:
running 1 test test tests::data_race_test ... error: Undefined Behavior: Data race detected between (1) non-atomic write on thread `tests::data_race_test` and (2) non-atomic read on thread `unnamed-2` at alloc1. (2) just happened here --> src/main.rs:5:18 | 5 | unsafe { A += 1 }; | ^^^^^^ Data race detected between (1) non-atomic write on thread `tests::data_race_test` and (2) non-atomic read on thread `unnamed-2` at alloc1. (2) just happened here | help: and (1) occurred earlier here --> src/main.rs:7:14 | 7 | unsafe { A += 1 }; | ^^^^^^ = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information = note: BACKTRACE (of the first span) on thread `unnamed-2`: = note: inside closure at src/main.rs:5:18: 5:24 note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace error: aborting due to 1 previous error
What About Libraries in C and C++?
Miri is an excellent tool, but one thing it can’t do, at least as of this writing, is interpret code called through the Rust Foreign Function Interface (FFI), which is how Rust calls libraries written in C and C++.[8] For example, FFI is how rusqlite calls SQLite which is written in C, how duckdb-rs calls DuckDB which written in C++, and how open62541 calls the OPC UA library written in C. Miri runs the programs using a platform-independent interpreter, so the program has no access to FFI or most platform-specific APIs.[9] Only a few common APIs are implemented in Miri, like basic file system access and printing to standard output.
The good news is we can return to using the sanitizers provided by compilers for C and C++, like GCC or Clang. The key is the C or C++ library code must be compiled with the appropriate sanitizer enabled before calling it from Rust.
Consider this unsafe code in C:
#include <stdio.h>
#include <string.h>
void c_say_hello(const char *message) {
char buffer[10];
strcpy(buffer, message); // Unsafe: no bounds checking!
printf("Hello from C! %s\n", buffer);
}
A build.rs
file can be used to compile the C code with Clang and enable the AddressSanitizer:
fn main() {
let mut build = cc::Build::new();
build
.compiler("clang")
.file("c_src/c_code.c")
.flag("-Wall") // Enable warnings
.flag("-fsanitize=address") // Enable AddressSanitizer
.flag("-fno-omit-frame-pointer"); // Simplify stack tracing
build.compile("c_code");
// Ensure the build script reruns if the C file changes
println!("cargo:rerun-if-changed=c_src/c_code.c");
}
The C code can be called from a Rust library in an unsafe block using FFI:
use std::ffi::{c_char, CString};
#[link(name = "c_code")] // Link to the compiled library
extern "C" {
fn c_say_hello(name: *const c_char);
}
pub fn say_hello(message: &str) {
let name = CString::new(message).expect("CString::new failed");
unsafe {
c_say_hello(name.as_ptr()); // Call the C function
}
}
Finally, the “safe” Rust function that wraps the C code can be called in a program:
use sanitizers::say_hello;
fn main() {
say_hello("This is far too long and will do bad things!");
}
Run the program with the AddressSanitizer enabled:
export RUSTFLAGS=-Zsanitizer=address
cargo +nightly run
It will report a stack-buffer-overflow error:
================================================================= ==51935==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016fa7e76a at pc 0x000100c32ad0 bp 0x00016fa7e730 sp 0x00016fa7dee0 WRITE of size 45 at 0x00016fa7e76a thread T0 #0 0x000100c32acc in strcpy+0x4ec (librustc-nightly_rt.asan.dylib:arm64+0x4aacc) #1 0x000100383ee8 in c_say_hello+0x11c (sanitizers:arm64+0x100003ee8) #2 0x000100383328 in sanitizers::say_hello::h7a86e3249bf087ea+0x1d8 (sanitizers:arm64+0x100003328) #3 0x0001003815c0 in sanitizers::main::h11beac415f2c6ee0 main.rs:4 #4 0x0001003818d8 in core::ops::function::FnOnce::call_once::h6f36f80e70ecb8a5 function.rs:250 #5 0x000100381910 in std::sys::backtrace::__rust_begin_short_backtrace::h5a6edce2cadaf2f4 backtrace.rs:152 #6 0x000100381488 in std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h6d09ba155578db90 rt.rs:195 #7 0x00010039ccfc in std::rt::lang_start_internal::hc996363c321dd410+0x440 (sanitizers:arm64+0x10001ccfc) #8 0x0001003812c0 in std::rt::lang_start::hf8df676e77f16e31 rt.rs:194 #9 0x0001003815ec in main+0x20 (sanitizers:arm64+0x1000015ec) #10 0x00019d87e0dc () #11 0xb922fffffffffffc ( ) Address 0x00016fa7e76a is located in stack of thread T0 at offset 42 in frame #0 0x000100383dd8 in c_say_hello+0xc (sanitizers:arm64+0x100003dd8) This frame has 1 object(s): [32, 42) 'buffer' (line 5) <== Memory access at offset 42 overflows this variable HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions *are* supported) SUMMARY: AddressSanitizer: stack-buffer-overflow (librustc-nightly_rt.asan.dylib:arm64+0x4aacc) in strcpy+0x4ec Shadow bytes around the buggy address: 0x00016fa7e480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016fa7e500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016fa7e580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016fa7e600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016fa7e680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x00016fa7e700: 00 00 00 00 00 00 00 00 f1 f1 f1 f1 00[02]f3 f3 0x00016fa7e780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016fa7e800: 00 00 00 00 f1 f1 f1 f1 f8 f8 f8 f8 f2 f2 f2 f2 0x00016fa7e880: 00 00 f3 f3 00 00 00 00 00 00 00 00 00 00 00 00 0x00016fa7e900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00016fa7e980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==51935==ABORTING
Note that the error is detected in the C stack frames when calling the strcpy
function inside the c_say_hello
function—the C code called through FFI is no longer opaque. If the C code in this example is compiled without the AddressSanitizer (try removing that line from build.rs
) but the Rust code is still run with the AddressSanitizer, the program will still fail with a stack-buffer-overflow, but the error will be less specific and identified in the Rust code not the C code.[10]
Conclusion
This article explored three techniques for verifying unsafe Rust code to make it safer and ensure it avoids undefined behaviours that can result in serious operational consequences, everything from malfunctions, security vulnerabilities, regulatory violations, economic loss, human injury, and death. This article was not intended to be an exhaustive survey, more a story of what I have explored to solve practical problems, including Rust crates that call C or C++ through FFI. The three techniques were: 1) sanitizers to check unsafe Rust code at run-time, 2) Miri, an interpreter for unsafe Rust code, and 3) sanitizers for run-time checks of C and C++ code called from Rust through FFI.
The majority of systems programmers and application developers should never write unsafe code in Rust. Unsafe Rust should largely be the domain of library developers. But if you must, and even when you call unsafe code through the libraries you include, I encourage you to test your code with sanitizers or Miri to avoid whole classes of errors.
In my next article, I will continue exploring tools to find errors in Rust, including both safe and unsafe code.
There is necessarily a significant amount of unsafe code in the Rust standard library and AWS has started an initiative to crowd-source the verification: Verify the Safety of the Rust Standard Library. AWS is investing heavily in Rust. Marc Brooker explains some of the reasons they used Rust for Amazon Aurora DSQL in the talk Deep dive into Amazon Aurora DSQL and its architecture from re:Invent 2025. ↩︎
Some of the examples are a bit contrived, but I want them to be simple and easy to understand. ↩︎
My next blog post will explore techniques for finding panics like this even in safe code. ↩︎
This example is modified from the Rust AddressSanitizer documentation. ↩︎
The program may crash when run on other operating systems or architectures—we can no longer count on the behaviour of the program when it encounters undefined behavior. ↩︎
The AddressSanitizer can also detect use after free, double free, memory leaks, and more, but these are not the memory errors of interest in this example. ↩︎
For a deeper look into how some of the LLVM sanitizers work, including the AddressSanitizer, see the talk Sanitize your C++ code by Kostya Serebryany from CppCon 2014. ↩︎
Because Miri reports errors for unsupported FFI code, one of the things you’ll quickly notice when adopting Miri is how much Rust code makes use of potentially unsafe code through FFI. ↩︎
This is an interesting feature of Miri. It means Miri can cross-interpret code and you can test code for targets that are different than your operating system. See Miri issue #4007 as an example. ↩︎
The ControlFlowIntegrity sanitizer is another tool that can be used with C and C++ code called through FFI to prevent invalid or malicious changes to the program’s control flow. ↩︎