In Rust, it is well-known that static mut is dangerous. The docs for the static keyword have this to say about mutable statics:
If a static item is declared with the mut keyword, then it is allowed to be modified by the program. However, accessing mutable statics can cause undefined behavior in a number of ways, for example due to data races in a multithreaded context. As such, all accesses to mutable statics require an unsafe block.
This makes perfect sense, so far. But my program is single threaded. I know it is single threaded because it runs on bare metal with no_std. I also know that I will need global state to manage access to memory-mapped I/O, and the global state needs to be mutable so that I can replace it at runtime. I can do this with static mut because I do not have to worry about data races with multiple threads right?
Well, that's the thought I initially had, anyway. And I believe I am not alone. But there is a lot of subtlety with static mut references that are problematic, even in a guaranteed single-threaded context. At first I believed it all has to do with the special 'static lifetime, but that is not the case. Rust By Example describes this lifetime as follows:
As a reference lifetime 'static indicates that the data pointed to by the reference lives for the entire lifetime of the running program. It can still be coerced to a shorter lifetime.
That also makes sense, and it's exactly what I want. In my case, the memory-mapped I/O that I wanted to control was a serial port. The primary goal was to reproduce something akin to standard I/O with println! and dbg! macros like those provided by the Rust standard library. A secondary goal was to make the sink configurable at runtime. This would allow the software to run on hardware with different serial interfaces, for example, without recompiling it for each distinct device.
The platform I was targeting is the Nintendo 64. Which, as you might guess, does not have your good old RS-232 serial port. It has a slot for game cartridges, and an expansion bus on the bottom which is basically just another cartridge port. It also has some controller ports on the front. Nothing really stands out as a plug-and-play means to get a serial console interface. As luck would have it, there are development cartridges that have an FTDI chip and USB port on them for exactly this purpose! The bad news is that each dev cart has its very own custom MMIO interface, and none of them are compatible.
Going back in time a bit, there are also debug interfaces on the original development hardware used in the 90's (and for a few years in the early 2000's) which use either parallel or SCSI ports. Some emulators support these interfaces because games (especially prototypes) actually print interesting things to them.
So we're in a situation where we want to write software for a platform (N64) that has a number of different serial interfaces available, but we don't want to build 3 or 4 different executables and put the burden on the user to figure out which one they need. And so here we are; we make the serial I/O implementation configurable at runtime. It can detect which interface is available, and that's what it uses. No sweat!
The core:fmt::Write trait
We want to use core::fmt::Write to make the implementation generic. This trait provides some useful methods for implementing the macros, and the core formatting machinery takes care of the details I don't want to think about. There are far better designs than what I came up with, but the initial implementation of my runtime-configurable I/O API would store a trait object (implementing fmt::Write) in a lazy_static. And the macros just needed a way to get a mutable reference to it. That's &'static mut dyn fmt::Write, mind you. But, I thought to myself, if I just hide all that inside the macro, nothing bad can happen!
What I didn't realize is that static mut has superpowers. I'll try my best to explain what these superpowers are, but please bear with me! I do not fully understand why these superpowers exist or what they are fully capable of. What I do know is that you never, ever, under any circumstance, want to anger them.
The 'static lifetime means that it lives forever (as far as the program is concerned) and making a static mutable means that you can only touch it in unsafe code, because threads and data races, right? Wrong! You need unsafe because the superpowers of static mut allow you to introduce instant Undefined Behavior. To explain, let's revisit what &mut means in the first place.
It's true that &mut allows you to change the contents of the thing behind it, but that's really just a side effect of the real purpose of &mut; it's an exclusive (unique) borrow. There can be only one.
All jokes aside, this is an invariant that you cannot break. Creating mutable aliases is instantly Undefined Behavior. Invoking Undefined Behavior gives the compiler license to do anything to your code. We really only want the compiler to do what we want it to do with our code, so let's just agree to avoid UB at all costs. To really nail this one down, consider what the Nomicon has to say about transmute:
- Transmuting an & to &mut is UB.
- Transmuting an & to &mut is always UB.
- No you can't do it.
- No you're not special.
There is some subtlety here that I won't go into, but the core of the issue is that this creates mutable aliases. Don't do it!
A starting point
There are some things that you will naturally begin to intuit while writing Rust after you have been using it for a while. It's helpful to recognize things that will cause the borrow checker to reject your code, for example. A quick internal borrow check in your brain can save a few seconds (or minutes, in extreme cases) of compile time just to be provided with an error message.
Let's begin with this bare minimum State struct:
#[derive(Copy, Clone, Debug)]
struct State {
x: i32,
}
impl State {
const fn new() -> Self {
Self { x: 0 }
}
fn get_mut(&mut self) -> &mut i32 {
&mut self.x
}
}
There is nothing really special here, so far. You can gain a mutable reference to the x field if you have mutable access to the struct. Standard stuff, so far. If you attempted to write code like the following, it might make you pause as your internal borrow checker raises a red flag:
fn main() {
let mut state = State::new();
let a = state.get_mut();
let b = state.get_mut();
*a = 42;
println!("I have two i32s: {}, {}", a, b);
}
Importantly, it isn't the explicit dereference (*a = 42;) that trips the borrow checker, here. It's the println! macro. And the error message illustrates this perfectly:
error[E0499]: cannot borrow `state` as mutable more than once at a time
--> src/main.rs:22:13
|
21 | let a = state.get_mut();
| ----- first mutable borrow occurs here
22 | let b = state.get_mut();
| ^^^^^ second mutable borrow occurs here
23 |
24 | println!("I have two i32s: {}, {}", a, b);
| - first borrow later used here
This mutable aliasing issue is something I've become more aware of over time, so it's kind of silly for me to write examples like this. The demonstration does help prove my point about superpowers though.
Let's make it static
Using the same State struct from above, let's make some minor adjustments to the program that uses it, so that State becomes static:
static STATE: State = State::new();
fn main() {
let a = STATE.get_mut();
*a = 42;
println!("I have one i32: {}", a);
}
Your internal borrow checker should raise another red flag, here. We made the state static, but not mutable, so we cannot compile this code:
error[E0596]: cannot borrow immutable static item `STATE` as mutable
--> src/main.rs:20:17
|
20 | let a = STATE.get_mut();
| ^^^^^ cannot borrow as mutable
Everything is as expected so far. We need to make the static state mutable so that we can borrow it mutably (exclusively, remember). And that also means we need to use the unsafe keyword. We're about to find out why it's actually unsafe. :)
static mut STATE: State = State::new();
fn main() {
unsafe {
let a = STATE.get_mut();
*a = 42;
println!("I have one i32: {}", a);
}
}
Well, that's not so bad! My internal borrow checker can't spot anything wrong with that. And neither can the compiler's (much better) borrow checker:
I have one i32: 42
But now I want to get spicy! 🔥 I want two exclusive references to the same memory. Or maybe I don't, but it accidentally happens anyway.
static mut STATE: State = State::new();
fn main() {
unsafe {
let a = STATE.get_mut();
let b = STATE.get_mut();
*a = 42;
println!("I have two i32s: {}, {}", a, b);
}
}
My internal borrow checker says this cannot work, just like it didn't work before. And the Rust borrow checker is way better than mine, so surely it won't allow this!
I have two i32s: 42, 42
ಠ_à²
(╯°□°)╯︵ ┻━┻
To be fair, trying to run this code in miri does detect the Undefined Behavior. This is a very powerful tool, along with the sanitizers. Always use these when you are writing unsafe code! No exceptions. This is a huge relief, or at the very least it's better than nothing. Here's what miri finds:
error: Undefined Behavior: no item granting write access to tag <2843> at alloc1 found in borrow stack.
--> src/main.rs:23:9
|
23 | *a = 42;
| ^^^^^^^ no item granting write access to tag <2843> at alloc1 found in borrow stack.
Still, you may be caught off guard by this. Don't worry, it's a perfectly normal reaction. We didn't fundamentally change the State implementation, and yet putting it into a static mut changes its semantics. We might be given a hint about why this is if we desugar the get_mut method's function signature:
fn get_mut(&'static mut self) -> &'static mut i32
Go ahead, try it out! It compiles just the same. The static lifetime was simply elided. But wait, what if we keep this 'static lifetime and go back to the safe program code that puts State on the stack?
error[E0597]: `state` does not live long enough
--> src/main.rs:21:13
|
21 | let a = state.get_mut();
| ^^^^^----------
| |
| borrowed value does not live long enough
| argument requires that `state` is borrowed for `'static`
...
31 | }
| - `state` dropped here while still borrowed
So it is true that the 'static lifetime is special. We cannot borrow State for 'static because State lives on the stack. We would have to move it to the heap and leak the reference to make it live for 'static if we wanted to call the method with this lifetime bound. However, we cannot create mutable aliases in safe code with leaked boxes because they are not static.
In the case of static mut, we're allowed to borrow with a 'static lifetime, but we're also allowed to exclusively borrow multiple times. If you could only exclusively borrow a static mut once, it would make the feature useless for many valid cases. And this is the tricky part that, at least in my mind, makes static mut such a nightmare. You don't have a choice but to ensure the invariant that mutable aliasing does not occur, but the compiler won't prevent you from doing so. It is particularly insidious when your safe interface allows reentrancy, and that's how I hit this problem. Nested dbg! macro calls are pretty common.
This is the superpower of static mut.
How did I fix this in my configurable serial I/O?
Well, I'm not entirely sure that I did, to be honest! But I did find a way to get my macros to receive an exclusive borrow with a lifetime shorter than 'static. And I did that by not borrowing a static mut. Instead I have statics with interior mutability (STDOUT and STDERR). I opted to use spin::Mutex<RefCell<T>> to keep my uses of the unsafe keyword to a minimum, but it's also possible to store state with interior mutability in a static UnsafeCell. This is the recommended replacement according to Consider deprecation of UB-happy `static mut` · Issue #53639 · rust-lang/rust (github.com)
I also had to jump through hoops to get trait objects working without Box, but that was another rabbit hole entirely. In the end, I learned a valuable lesson about static mut and how bad it is for your health (and mine!) There are ways to make it work that are trivially safe, but reading through that GitHub ticket ought to ruffle your feathers, even if I haven't already convinced you that static mut probably should not be used.
Something cool I discovered some time ago about defmt, was that it tried to solve this particular re-entrancy problem by making proper traits at the API level, which I really liked. The `Logger` trait uses two methods instead of one to acquire and then release the logger. Neat!
ReplyDeleteThis could actually be really useful in my setup. defmt offers the common macros for printing format strings, but also has log features. Awesome! Thanks for the suggestion.
ReplyDelete