> What worries me is that when it comes to safety, surely the chain is as strong as the weakest link? You declare one thing unsafe, and then suddenly, all bets are off. And maybe you don't even know that what you're doing is unsafe.

Yes, you are correct. If some code or module contains a memory safety or data race bug, then the code which uses it could realize that bug in the form of a crash or race. This paper is not analyzing code that contains a memory safety or data race bug, but instead, code that the Rust compiler cannot prove doesn't have such a bug. That is a big difference.

Let's take your example. The questions you bring up are good ones. Fiddling with registers may indeed require atomic in the presence of multiple threads using that register. Or, if you can guarantee only one thread touches those registers, it may not. In both use cases, I'd like the compiler to help me verify that those conditions are met. A Rust approach to this problem (verified empirically by the paper) would be to create a module, function, or trait that enforces the invariants we need. Such an abstraction might contain a bug that causes the codebase to be unsafe. But when debugging that, we shouldn't have to look over the whole codebase for register writes but rather in just one place.

> Suppose you have an ISR (interrupt service routine) that wants to toggle a GPIO pin. From whence does it borrow the privilege to set the pin?

Assuming we are protecting the pin with atomics, the ISR could call into our library to acquire the lock. The ISR doesn't necessarily need to borrow the privilege from another thread. Our module could provide a safe interface for unsafe access to a global lock.

I would also ask how in C/C++ an ISR performing such an operation would work without data races. I assume it would need to perform a very similar set of operations to the compiled Rust binary. The only difference is that it is harder to find that gpio pin manipulation in the C code. That isn't an accident Rust's explicit goal is for its abstractions to be zero-cost.

> Surely, at the end of the day, microcontrollers can be characterized as large state machines with global state. That's what they ARE. It's their inherent nature, which you cannot abstract away without it leaking somewhere (at least to do anything useful, anyway).

First, this can be said about any computer, no matter how small or large. But I think that you are getting at something important here. Rust is a much more natural fit in user space on an operating system that abstracts away the hardware details than in a micro-controller. And certainly, on a microcontroller, you have to interact often with IO of various kinds, which are global state that Rust considers unsafe. But I think, to the extent, it is possible having the unsafe/safe stratification of code enables you to separate the code which doesn't interact with the outside world and the code which does. I think that is abstractly similar to the user/kernel space divide and Haskell's IO/computation divide. I believe that split helps with code organization and concentrates the attention when such a bug occurs.

> At least with C/C++ you KNOW you've got a problem, and have to reason about the problem carefully.

First, you should know that Rust's safety guarantees extend only to memory safety and data races. Second, I don't think that it is true, in C/C++, that if you have a memory safety or data race bug, you necessarily know that you have one. If you do have one, it could be in any part of the code. Reasoning about a large codebase is much more difficult than a single module with all potentially unsafe code.

> With Rust, you THINK you've solved the problem, but have you?

You may well not have. Here is an example of Rust's core libraries containing a CVE [1]. And here is another popular crate that had a CVE relating to unsafe code [2].

The paper noted the unsafe code was slightly more prevalent amongst the most popular repos. At first, that may seem like a bad thing, but I take this as a good sign because it means the unsafe code is more concentrated in few crates. And as a result of that, more attention can be placed on making sure those crates are correct. If they are safe, then the Rust compiler can ensure that client code is safe. Compare that to C, where if you decide to use someone else's code, you and you alone are responsible for ensuring you don't break the invariants they expected of you. The borrow checking Rust does to verify the safety property is a form of automatic verification that is good news for everyone. We know empirically that 70% of all security vulnerabilities are related to memory safety [3][4]. And that is not due to a lack of talented programmers (those stats come from Google and Microsoft). I would want the compiler to ensure most of my code doesn't have these issues. Rust enables the compiler to do that, whereas C/C++ stops a compiler from doing that at the same level. And so we must fall back on tools like ASAN, which, while good, don't happen early enough in the code-debug loop and have proven (see previous evidence) not to solve the problem well enough.

There is another part to this story that I don't expect most or even a significant fraction of Rust programmers to be proficient in but which we all can benefit from, formal verification. There is a substantial academic (and to a lesser extent industry) driven push to create formal verification tools [5]. I've seen a bug report on the standard library that a graduate student found while verifying a module that used unsafe code. That bug report was a direct benefit to all the users of that module. In an equivalent C world, the code users would need whole program formal verification actually to ensure they got a similar benefit. With Rust, the compiler is doing that heavy lifting for us.

The paper notes that unsafe rules are still provisional (meaning only that they are subject to change, not incomplete), but there is a goal to formalize those rules. Standardizing those rules enables those who write unsafe code to prove (not just test) that their module/function/trait properly encapsulates the unsafe code in such a way that it cannot cause memory safety or data race bugs. If you can do that, you can KNOW that you don't have a problem anywhere in the code. Now, of course, the standard disclaimers around formal verification apply: most people won't do this. It requires specialized skill and knowledge, and so on. But there are limited cases where it would confer a real benefit, the Rust standard library, being the most widely used, is perhaps the prime example.

I'd also like to note that Rust raises the bar in many other ways you may not be aware of. Integer manipulation is a good example. Rust will throw a checked runtime error in debug builds when there is a numerical overflow or underflow. If that is not the behavior, you expected you could use a compiler intrinsic made available in the standard library to explicitly allow and specify the exact behavior you expected in the event of an overflow [6]. That's important for the correctness of integer manipulation and memory safety if a computed index contains an unexpected integer overflow. There are other examples of Rust raising the bar above C, including hygienic macros, strings, and algebraic data types [7][8]. And it does that without compromising on performance and sometimes enabling more performant patterns, which would be possible but not advisable to attempt in C [9].

[1] https://cve.mitre.org/cgi-bin/cvename.cgi?name=%20CVE-2018-1...
[2] https://shnatsel.medium.com/auditing-popular-rust-crates-how...
[3] https://www.zdnet.com/article/chrome-70-of-all-security-bugs...
[4] https://msrc-blog.microsoft.com/2019/07/16/a-proactive-appro...
[5] https://alastairreid.github.io/rust-verification-tools/
[6] https://doc.rust-lang.org/std/primitive.i64.html
[7] http://dtrace.org/blogs/bmc/2018/09/18/falling-in-love-with-...
[8] http://dtrace.org/blogs/bmc/2020/10/11/rust-after-the-honeym...
[9] http://dtrace.org/blogs/bmc/2018/09/28/the-relative-performa...