0% found this document useful (0 votes)
40 views9 pages

Safe Systems Programming with Rust

Rust is a programming language that combines safety and control, overcoming the tradeoff seen in traditional languages like Java and C++. It employs a strong type system with ownership and borrowing concepts to prevent memory safety violations and data races at compile time. The RustBelt project aims to formalize Rust's safety claims, encouraging further exploration and development within the programming community.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
40 views9 pages

Safe Systems Programming with Rust

Rust is a programming language that combines safety and control, overcoming the tradeoff seen in traditional languages like Java and C++. It employs a strong type system with ownership and borrowing concepts to prevent memory safety violations and data races at compile time. The RustBelt project aims to formalize Rust's safety claims, encouraging further exploration and development within the programming community.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Safe Systems Programming in Rust:

The Promise and the Challenge

Ralf Jung Jacques-Henri Jourdan Robbert Krebbers Derek Dreyer


MPI-SWS, Université Paris-Saclay, CNRS, Radboud University MPI-SWS,
Germany ENS Paris-Saclay, LMF, Nijmegen, Germany
France The Netherlands

fine-grained control over resource management. However,


Key Insights this control comes at a steep cost. For example, Microsoft
• Rust is the first industry-supported programming language recently reported that 70% of the security vulnerabilities they
to overcome the longstanding tradeoff between the safety fix are due to memory safety violations [33], precisely the
guarantees of higher-level languages (like Java) and the type of bugs that strong type systems were designed to rule
control over resource management provided by lower-level out. Likewise, Mozilla reports that the vast majority of critical
“systems programming” languages (like C and C++). bugs they find in Firefox are memory-related [16]. If only
• It tackles this challenge using a strong type system based there were a way to somehow get the best of both worlds: a
on the ideas of ownership and borrowing, which statically safe systems programming language with control. . .
prohibits the mutation of shared state. This approach en- Enter Rust. Sponsored by Mozilla and developed actively
ables many common systems programming pitfalls to be over the past decade by a large and diverse community of
detected at compile time. contributors, Rust supports many common low-level program-
• There are a number of data types whose implementations ming idioms and APIs derived from modern C++. However,
fundamentally depend on shared mutable state and thus unlike C++, Rust enforces the safe usage of these APIs with
cannot be typechecked according to Rust’s strict ownership a strong static type system.
discipline. To support such data types, Rust embraces the In particular, like Java, Rust protects programmers from
judicious use of unsafe code encapsulated within safe APIs.
memory safety violations (e.g., “use-after-free” bugs). But
• The proof technique of semantic type soundness, together Rust goes further by defending programmers against other,
with advances in separation logic and machine-checked more insidious anomalies that no other mainstream language
proof, has enabled us to begin building rigorous formal can prevent. For example, consider data races: unsynchro-
foundations for Rust as part of the RustBelt project.
nized accesses to shared memory (at least one of which is
a write). Even though data races effectively constitute unde-
There is a longstanding tension in programming language fined (or weakly-defined) behavior for concurrent code, most
design between two seemingly irreconcilable desiderata. “safe” languages (such as Java and Go) permit them, and they
are a reliable source of concurrency bugs [35]. In contrast,
• Safety. We want strong type systems that rule out large Rust’s type system rules out data races at compile time.
classes of bugs statically. We want automatic memory Rust has been steadily gaining in popularity, to the point
management. We want data encapsulation, so that we can that it is now being used internally by many major indus-
enforce invariants on the private representations of objects trial software vendors (such as Dropbox, Facebook, Ama-
and be sure that they will not be broken by untrusted code. zon, and Cloudflare) and has topped Stack Overflow’s list
• Control. At least for “systems programming” applications of “most loved” programming languages for the past four
like web browsers, operating systems, or game engines, years. Microsoft’s Security Response Center Team recently
where performance or resource constraints are a primary announced that it is actively exploring an investment in the
concern, we want to determine the byte-level representa- use of Rust at Microsoft to stem the tide of security vulnera-
tion of data. We want to optimize the time and space usage bilities in system software [25, 8].
of our programs using low-level programming techniques. The design of Rust draws deeply from the wellspring of
We want access to the “bare metal” when we need it. academic research on safe systems programming. In particu-
lar, the most distinctive feature of Rust’s design—in relation
Sadly, the conventional wisdom goes, we can’t have to other mainstream languages—is its adoption of an own-
everything we want. Languages like Java give us strong ership type system (which in the academic literature is often
safety, but it comes at the expense of control. As a result, for referred to as an affine or substructural type system [36]).
many systems programming applications, the only realistic Ownership type systems help the programmer enforce safe
option is to use a language like C or C++ that provides patterns of lower-level programming by placing restrictions

1 2021/2/22
on which aliases (references) to an object may be used to existing elements are moved over. Let us assume this is what
mutate it at any given point in the program’s execution. happens here. Why is this case interesting? Because vptr
However, Rust goes beyond the ownership type systems still points to the old buffer. In other words, adding a new
of prior work in at least two novel and exciting ways: element to v has turned vptr into a dangling pointer. This
is possible because both pointers were aliasing: an action
1. Rust employs the mechanisms of borrowing and lifetimes,
through a pointer (v) will in general also affect all its aliases
which make it much easier to express common C++-style
(vptr). The entire situation is visualized as follows:
idioms and ensure that they are used safely.
v
2. Rust also provides a rich set of APIs—e.g., for concur- 10 10
rency abstractions, efficient data structures, and memory vptr 11 11
management—which fundamentally extend the power of 12
the language by supporting more flexible combinations The fact that vptr is now a dangling pointer becomes
of aliasing and mutation than Rust’s core type system al- a problem in the fourth line. Here we load from vptr, and
lows. Correspondingly, these APIs cannot be implemented since it is a dangling pointer, this is a use-after-free bug.
within the safe fragment of Rust: rather, they internally In fact, the problem is common enough that one instance of
make use of potentially unsafe C-style features of the lan- it has its own name: iterator invalidation, which refers to the
guage, but in a safely encapsulated way that is claimed situation where an iterator (usually internally implemented
not to disturb Rust’s language-level safety guarantees. with a pointer) gets invalidated because the data structure it it-
These aspects of Rust’s design are not only essential to erates over is mutated during the iteration. It most commonly
its success—they also pose fundamental research questions arises when one iterates over some container data structure in
about its semantics and safety guarantees that the program- a loop, and indirectly, but accidentally, calls an operation that
ming languages community is just beginning to explore. mutates the data structure. Notice that in practice the call to
In this article, we begin by giving the reader a bird’s-eye the operation that mutates the data structure (push_back in
view of the Rust programming language, with an emphasis line 3 of our example) might be deeply nested behind several
on some of the essential features of Rust that set it apart from layers of abstraction. In particular when code gets refactored
its contemporaries. Second, we describe the initial progress or new features get added, it is often near impossible to de-
made in the RustBelt project, an ongoing project funded termine if pushing to a certain vector will invalidate pointers
by the European Research Council (ERC), whose goal is to elsewhere in the program that are going to be used again later.
provide the first formal (and machine-checked) foundations Comparison with garbage-collected languages
for the safety claims of Rust. In so doing, we hope to inspire
other members of the computer science research community Languages like Java, Go, and OCaml avoid use-after-free
to start paying closer attention to Rust and to help contribute bugs using garbage collection: memory is only deallocated
to the development of this groundbreaking language. when it can no longer be used by the program. Thus, there
can be no dangling pointers and no use-after-free.
One problem with garbage collection is that, to make it effi-
Motivation: Pointer Invalidation in C++ cient, such languages generally do not permit interior pointers
To demonstrate the kind of memory safety problems that (i.e., pointers into data structures). For example, arrays int[]
arise commonly in systems programming languages, let us in Java are represented similarly to std::vector<int> in
consider the following C++ code: C++ (except arrays in Java cannot be grown). However, un-
like in C++, one can only get and set elements of a Java array,
1 std::vector<int> v { 10, 11 };
not take references to them. To make the elements themselves
2 int *vptr = &v[1]; // Points *into* ‘v‘.
addressable, they need to be separate objects, references to
3 v.push_back(12);
which can then be stored in the array—i.e., the elements need
4 std::cout << *vptr; // Bug (use-after-free)
to be “boxed”. This sacrifices performance and control over
In the first line, this program creates a std::vector (a memory layout in return for safety.
growable array) of integers. The initial contents of v, the two On top of that, garbage collection does not even properly
elements 10 and 11, are stored in a buffer in memory. In the solve the issue of iterator invalidation. Mutating a collection
second line, we create a pointer vptr that points into this while iterating over it in Java cannot lead to dangling pointers,
buffer; specifically it points to the place where the second but it may lead to a ConcurrentModificationException
element (with current value 11) is stored. Now both v and being thrown at run time. Similarly, while Java does prevent
vptr point to (overlapping parts of) the same buffer; we say security vulnerabilities caused by null pointer misuse, it does
that the two pointers are aliasing. In the third line, we push so with run-time checks that raise a NullPointerException.
a new element to the end of v. The element 12 is added In both of these cases, while the result is clearly better than
after 11 in the buffer backing v. If there is no more space for the corresponding undefined behavior of a C++ program, it
an additional element, a new buffer is allocated and all the still leaves a lot to be desired: instead of shipping incorrect

2 2021/2/22
code and then detecting issues at run time, we want to prevent compile-time error. This is because Rust considers ownership
the bugs from occurring in the first place. of v to have moved to consume as part of the call, meaning
that consume can do whatever it desires with w, and the caller
Rust’s solution to pointer invalidation may no longer access the memory backing this vector at all.
In Rust, issues like iterator invalidation and null pointer Resource management. Ownership in Rust not only pre-
misuse are detected statically, by the compiler—they lead to a vents memory bugs—it also forms the core of Rust’s approach
compile-time error instead of a run-time exception. To explain to memory management and, more generally, resource man-
how this works, consider the following Rust translation of agement. When a variable holding owned memory (e.g., a
our C++ example: variable of type Vec<T>, which owns the buffer in memory
1 let mut v = vec![10, 11]; backing the vector) goes out of scope, we know for sure that
2 let vptr = &mut v[1]; // Points *into* ‘v‘. this memory will not be needed any more—so the compiler
3 [Link](12); can automatically deallocate the memory at that point. To
4 println!("{}", *vptr); // Compiler error this end, the compiler transparently inserts destructor calls,
just like in C++. For example, in the consume function, it is
Like in the C++ version, there is a buffer in memory, and vptr not actually necessary to call the destructor method (drop)
points into the middle of that buffer (causing aliasing); push explicitly. We could have just left the body of that function
might reallocate the buffer, which leads to vptr becoming a empty, and it would have automatically deallocated w itself.
dangling pointer, and that leads to a use-after-free in line 4. As a consequence, Rust programmers rarely have to worry
But none of this happens; instead the compiler shows an about memory management: it is largely automatic, despite
error: “cannot borrow v as mutable more than once at a time”. the lack of a garbage collector. Moreover, the fact that mem-
We will come back to “borrowing” soon, but the key idea— ory management is also static (determined at compile time)
the mechanism through which Rust achieves memory safety yields enormous benefits: it helps not only to keep the max-
in the presence of pointers that point into a data structure— imal memory consumption down, but also to provide good
already becomes visible here: the type system enforces the worst-case latency in a reactive system such as a web server.
discipline (with a notable exception that we will come to And on top of that, Rust’s approach generalizes beyond mem-
later) that a reference is never both aliased and mutable at ory management: other resources like file descriptors, sockets,
the same time. This principle should sound familiar in the lock handles, and so on are handled with the same mecha-
context of concurrency, and indeed Rust uses it to ensure nism, so that Rust programmers do not have to worry, for
the absence of data races as well. However, as our example instance, about closing files or releasing locks. Using destruc-
that is rejected by the Rust compiler shows, the unrestricted tors for automatic resource management was pioneered in
combination of aliasing and mutation is a recipe for disaster the form of RAII (Resource Acquisition Is Initialization) in
even for sequential programs: in line 3, vptr and v alias (v C++ [31]; the key difference in Rust is that the type system
is considered to point to all of its contents, which overlaps can statically ensure that resources do not get used any more
with vptr), and we are performing a mutation, which would after they have been destructed.
lead to a memory access bug in line 4.
Mutable references. A strict ownership discipline is nice
and simple, but unfortunately not very convenient to work
Ownership and Borrowing
with. Frequently, one wants to provide data to some function
The core mechanism through which Rust prevents uncon- temporarily, but get it back when that function is done. For
trolled aliasing is ownership. Memory in Rust always has a example, we want [Link](12) to grant push the privilege
unique owner, as demonstrated by the following example: to mutate v, but we do not want it to consume the vector v.
1 fn consume(w: Vec<i32>) { In Rust, this is achieved through borrowing, which takes
2 drop(w); // deallocate vector a lot of inspiration from prior work on region types [34, 13].
3 } For example, we could write:
4 let v = vec![10, 11]; 1 fn add_something(v: &mut Vec<i32>) {
5 consume(v); 2 [Link](11);
6 [Link](12); // Compiler error 3 }
4 let mut v = vec![10];
Here, we construct v similar to our first example, and then
5 add_something(&mut v);
pass it to consume. Operationally, just like in C++, parame-
6 [Link](12); // Ok!
ters are passed by value but the copy is shallow—pointers get
7 // [Link](12) is syntactic sugar for
copied but their pointee does not get duplicated. This means
8 // Vec::push(&mut v, 12)
that v and w point to the same buffer in memory.
Such aliasing is a problem if v and w would both be used The function add_something takes an argument of type
by the program, but an attempt to do so in line 6 leads to a &mut Vec<i32>, which indicates a mutable reference to a

3 2021/2/22
Vec<i32>. Operationally, this acts just like a reference in it takes two closures, runs both of them in parallel, waits until
C++, i.e., the Vec is passed by reference. In the type system, both are done, and returns both of their results. When join
this is interpreted as add_something borrowing ownership returns, the borrow ends, so we can mutate v again.
of the Vec from the caller. Just like mutable references, shared references have a
The function add_something demonstrates what borrow- lifetime. Under the hood, the Rust compiler is using a lifetime
ing looks like in well-formed programs. To see why the com- to track the period during which v is temporarily shared
piler accepts that code while rejecting our pointer invalidation between the two threads; after that lifetime is over (on line 5),
example from page 3, we have to introduce another concept: the original owner of v regains full control. The key difference
lifetimes. Just like in real life, when borrowing something, here is that multiple shared references are allowed to coexist
misunderstanding can be prevented by agreeing up front on during the same lifetime, so long as they are only used for
how long something may be borrowed. So, when a reference reading, not writing. We can witness the enforcement of this
gets created, it gets assigned a lifetime, which gets recorded restriction by changing one of the two threads in our example
in the full form of the reference type: &’a mut T for life- to || [Link](12): then the compiler complains that we
time ’a. The compiler ensures that (a) the reference (v, in cannot have a mutable reference and a shared reference to
our example) only gets used during that lifetime, and (b) the the Vec at the same time. And indeed, that program has a
referent does not get used again until the lifetime is over. fatal data race between the reading thread and the thread
In our case, the lifetimes (which are all inferred by the that pushes to the vector, so it is important that the compiler
compiler) just last for the duration of add_something and detects such cases statically.
Vec::push, respectively. Never is v used while the lifetime Shared references are also useful in sequential code; for
of a previous borrow is still ongoing. example, while doing a shared iteration over a vector we can
In contrast, consider the example from page 3: still pass a shared reference to the entire vector to another
function. But for this article, we will focus on the use of
1 let mut v = vec![10, 11];
sharing for concurrency.
2 let vptr : &’a mut i32 = &mut v[1];
3 [Link](12);
4 println!("{}", *vptr); // Compiler error Summary
Lifetime’a In order to obtain safety, the Rust type system
enforces the discipline that a reference is never
The lifetime ’a of the borrow for vptr starts in line 2 and Aliasing
both aliased and mutable. Having a value of
goes on until line 4. It cannot be any shorter because vptr +
type T means you “own” it fully. The value
gets used in line 4. However, this means that in line 3, v is Mutation
of type T can be “borrowed” using a mutable
used while an outstanding borrow exists, which is an error. reference (&mut T) or shared reference (&T).
To summarize: whenever something is passed by value (as
in consume), Rust interprets this as ownership transfer; when
something is passed by reference (as in add_something), Relaxing Rust’s Strict Ownership Discipline
Rust interprets this as borrowing for a certain lifetime. via Safe APIs
Shared references. Following the principle that we can Rust’s core ownership discipline is sufficiently flexible to
have either aliasing or mutation, but not both at the same account for many low-level programming idioms. But for im-
time, mutable references are unique pointers: they do not plementing certain data structures, it can be overly restrictive.
permit aliasing. To complete this picture, Rust has a second For example, without any mutation of aliased state, it is not
kind of reference, the shared reference written &Vec<i32> or possible to implement a doubly-linked list because each node
&’a Vec<i32>, which allows aliasing but no mutation. One is aliased by both its next and previous neighbors.
primary use-case for shared references is to share read-only Rust adopts a somewhat unusual approach to this problem.
data between multiple threads: Rather than either (1) complicating its type system to account
for data structure implementations that do not adhere to it, or
1 let v = vec![10,11];
(2) introducing dynamic checks to enforce safety at run time,
2 let vptr = &v[1];
Rust allows its ownership discipline to be relaxed through the
3 join( || println!("v[1] = {}", *vptr),
development of safe APIs—APIs that extend the expressive
4 || println!("v[1] = {}", *vptr));
power of the language by enabling safely controlled usage
5 [Link](12);
of aliased mutable state. Although the implementations of
Here, we create a shared reference vptr pointing to (and these APIs do not adhere to Rust’s strict ownership discipline
borrowing) v[1]. The vertical bars here represent a closure (a point we return to on page 6), the APIs themselves make
(also sometimes called an anonymous function or “lambda”) critical use of Rust’s ownership and borrowing mechanisms
that does not take any arguments. These closures are passed to ensure that they preserve the safety guarantees of Rust as a
to join, which is the Rust version of “parallel composition”: whole. Let us now look at a few examples.

4 2021/2/22
Shared mutable state Reference counting
Rust’s shared references permit multiple threads to read We have seen that shared references provide a way to share
shared data concurrently. But threads that just read data are data between different parties in a program. However, shared
only half the story, so next we will look at how the Mutex references come with a statically determined lifetime, and
API enables one to safely share mutable state across thread when that lifetime is over, the data is uniquely owned again.
boundaries. At first, this might seem to contradict everything This works well with structured parallelism (like join in
we said so far about the safety of Rust: isn’t the whole point the previous example), but does not work with unstructured
of Rust’s ownership discipline that it prevents mutation of parallelism where threads are spawned off and keep running
shared state? Indeed it is, but we will see how, using Mutex, independently from the parent thread.
such mutation can be sufficiently restricted so as to not break In Rust, the typical way to share data in such a situation is
memory or thread safety. Consider the following example: to use an atomically reference-counted pointer: Arc<T> is a
pointer to T, but it also counts how many such pointers exist
1 let mutex_v = Mutex::new(vec![10, 11]);
and deallocates the T (and releases its associated resources)
2 join(
when the last pointer is destroyed. (This can be viewed as a
3 || { let mut v = mutex_v.lock().unwrap();
form of lightweight library-implemented garbage collection.)
4 [Link](12); },
Since the data is shared, we cannot obtain an &mut T from
5 || { let v = mutex_v.lock().unwrap();
an Arc<T>—but we can obtain an &T (where the compiler
6 println!("{:?}", *v) });
ensures that during the lifetime of the reference, the Arc<T>
We again use structured concurrency and shared references, does not get destroyed), as in this example:
but now we wrap the vector in a Mutex: the variable mutex_v 1 let arc_v1 = Arc::new(vec![10, 11]);
has type Mutex<Vec<i32>>. The key operation on a Mutex 2 let arc_v2 = Arc::clone(&arc_v1);
is lock, which blocks until it can acquire the exclusive lock. 3 spawn(move || println!("{:?}", arc_v2[1]));
The lock implicitly gets released by v’s destructor when the 4 println!("{:?}", arc_v1[1]);
variable goes out of scope. Ultimately, this program prints
either [10, 11, 12] if the first thread manages to acquire We start by creating an Arc that points to our usual vector.
the lock first, or [10, 11] if the second thread does. arc_v2 is obtained by cloning arc_v1, which means that
In order to understand how our example program type- the reference count gets bumped up by one, but the data
checks, let us take a closer look at lock. It (almost1 ) has type itself is not duplicated. Then we spawn a thread that uses
fn(&’a Mutex<T>) -> MutexGuard<’a, T>. This type arc_v2; this thread keeps running in the background even
says that lock can be called with a shared reference to a mu- when the function we are writing here returns. Because this
tex, which is why Rust lets us call lock on both threads: both is unstructured parallelism we have to explicitly move (i.e.,
closures capture an &Mutex<Vec<i32>>, and as with the transfer ownership of) arc_v2 into the closure that runs
vptr of type &i32 that got captured in our first concurrency in the other thread. Arc is a “smart pointer” (similar to
example, both threads can then use that reference concur- shared_ptr in C++), so we can work with it almost as if it
rently. In fact, it is crucial that lock take a shared rather were an &Vec<i32>. In particular, in lines 3 and 4 we can
than a mutable reference—otherwise, two threads could not use indexing to print the element at position 1. Implicitly, as
attempt to acquire the lock at the same time and there would arc_v1 and arc_v2 go out of scope, their destructors get
be no need for a lock in the first place. called, and the last Arc to be destroyed deallocates the vector.
The return type of lock, namely MutexGuard<’a, T>, is Thread safety
basically the same as &’a mut T: it grants exclusive access
There is one last type that we would like to talk about in
to the T that is stored inside the lock. Moreover, when it goes
this brief introduction to Rust: Rc<T> is a reference-counted
out of scope, it automatically releases the lock (an idiom
type very similar to Arc<T>, but with the key distinction that
known in the C++ world as RAII [31]).
Arc<T> uses an atomic (fetch-and-add) instruction to update
In our example, this means that both threads temporarily
the reference count, whereas Rc<T> uses non-atomic memory
have exclusive access to the vector, and they have a mutable
operations. As a result, Rc<T> is potentially faster, but not
reference that reflects that fact—but thanks to the lock prop-
thread-safe. The type Rc<T> is useful in complex sequential
erly implementing mutual exclusion, they will never both
code where the static scoping enforced by shared references
have a mutable reference at the same time, so the unique-
is not flexible enough, or where one cannot statically predict
ness property of mutable references is maintained. In other
when the last reference to an object will be destroyed so that
words, Mutex can offer mutation of aliased state safely be-
the object itself can be deallocated.
cause it implements run-time checks ensuring that, during
Since Rc<T> is not thread-safe, we need to make sure that
each mutation, the state is not aliased.
the programmer does not accidentally use Rc<T> when they
1 Theactual type of lock wraps the result in a LockResult<...> for error should have used Arc<T>. This is important: if we take our
handling, which explains why we use unwrap on lines 3 and 5. previous Arc example, and replace all the Arc by Rc, the

5 2021/2/22
program has a data race and might deallocate the memory concurrent reasoning, and the Rust compiler simply has no
too early or not at all. However, quite remarkably, the Rust way to verify statically that deallocating the memory when
compiler is able to catch this mistake. The way this works is the reference count reaches zero is in fact safe.
that Rust employs something called the Send trait: a property Alternatives to unsafe blocks. One could turn things like
of types which is only enjoyed by a type T if elements of Arc or Vec into language primitives. For example, Python
type T can be safely sent to another thread. The type Arc<T> and Swift have built-in reference counting, and Python has
is Send, but Rc<T> is not. Both join and spawn require list as a built-in equivalent to Vec. However, these language
everything captured by the closure(s) they run to be Send,
features are implemented in C or C++, so they are not actually
so if we capture a value of the non-Send type Rc<T> in a
any safer than the unsafe Rust implementation. Beyond that,
closure, compilation will fail. restricting unsafe operations to implementations of language
Rust’s use of the Send trait demonstrates how sometimes primitives also severely restricts flexibility. For example,
the restrictions imposed by strong static typing can lead to Firefox uses a Rust library implementing a variant of Arc
greater expressive power, not less. In particular, C++’s smart without support for weak references, which improves space
reference-counted pointer, std::shared_ptr, always uses
usage and performance for code that does not need them.
atomic instructions2 , because having a more efficient non-
Should the language provide primitives for every conceivable
thread-safe variant like Rc is considered too risky. In contrast, spot in the design space of any built-in type?
Rust’s Send trait allows one to “hack without fear” [26]: it Another option to avoid unsafe code is to make the type
provides a way to have both thread-safe data structures (such system expressive enough to actually be able to verify safety
as Arc) and non-thread-safe data structures (such as Rc) in of types like Arc. However, due to how subtle correctness of
the same language, while ensuring modularly that the two do
such data structures can be (and indeed Arc and simplified
not get used in incorrect ways.
variants of it have been used as a major case-study in several
recent formal verification papers [12, 18, 9]), this basically
Unsafe Code, Safely Encapsulated requires a form of general-purpose theorem prover—and a
We have seen how types like Arc and Mutex let Rust pro- researcher with enough background to use it. The theorem
grams safely use features such as reference counting and proving community is quite far away from enabling develop-
shared mutable state. However, there is a catch: those types ers to carry out such proofs themselves.
cannot actually be implemented in Rust. Or, rather, they can- Safe abstractions. Rust has instead opted to allow pro-
not be implemented in safe Rust: the compiler would reject an grammers the flexibility of writing unsafe code when neces-
implementation of Arc for potentially violating the aliasing sary, albeit with the expectation that it should be encapsulated
discipline. In fact, it would even reject the implementation by safe APIs. Safe encapsulation means that, regardless of the
of Vec for accessing potentially uninitialized memory. For fact that Rust APIs like Arc or Vec are implemented with un-
efficiency reasons, Vec manually manages the underlying safe code, users of those APIs should not be affected: so long
buffer and tracks which parts of it are initialized. Of course, as users write well-typed code in the safe fragment of Rust,
the implementation of Arc does not in fact violate the alias- they should never be able to observe anomalous behaviors
ing discipline, and Vec does not in fact access uninitialized due to the use of unsafe code in the APIs’ implementation.
memory, but the arguments needed to establish those facts This is in marked contrast to C++, whose weak type system
are too subtle for the Rust compiler to infer. lacks the ability to even enforce that APIs are used safely. As
To solve this problem, Rust has an “escape hatch”: Rust a result, C++ APIs like shared_ptr or vector are prone
consists not only of the safe language we discussed so to misuse, leading to reference-counting bugs and iterator
far—it also provides some unsafe features such as C-style invalidation, which do not arise in Rust.
unrestricted pointers. The safety (memory safety and/or The ability to write unsafe code is like a lever that Rust
thread safety) of these features cannot be guaranteed by the programmers use to make the type system more useful
compiler, so they are only available inside syntactic blocks without turning it into a theorem prover, and indeed we
that are marked with the unsafe keyword. This way, one can believe this to be a key ingredient to Rust’s success. The
be sure to not accidentally leave the realm of safe Rust. Rust community is developing an entire ecosystem of safely
For example, the implementation of Arc uses unsafe code usable high-performance libraries, enabling programmers to
to implement a pattern that would not be expressible in safe build safe and efficient applications on top of them.
Rust: sharing without a clear owner, managed by thread-safe But of course, there is no free lunch: it is up to the author
reference counting. This is further complicated by support for of a Rust library to somehow ensure that, if they write unsafe
“weak references”: references that do not keep the referent code, they are being very careful not to break Rust’s safety
alive, but can be atomically checked for liveness and upgraded guarantees. On the one hand, this is a much better situation
to a full Arc. The correctness of Arc relies on rather subtle than in C/C++, because the vast majority of Rust code is
2 Moreprecisely, on Linux it uses atomic instructions if the program uses written in the safe fragment of the language, so Rust’s “attack
pthreads, i.e., if it or any library it uses might spawn a thread. surface” is much smaller. On the other hand, when unsafe

6 2021/2/22
code is needed, it is far from obvious how a programmer is Theorem 3 (Fundamental theorem). If a component e is
supposed to know if they are being “careful” enough. syntactically well-typed, then e satisfies its safety contract.
To maintain confidence in the safety of the Rust ecosystem,
Together, these imply that a Rust program is safe if the only
we therefore really want to have a way of formally specifying
appearances of unsafe blocks are within libraries that have
and verifying what it means for uses of unsafe code to be
been manually verified to satisfy their safety contracts.
safely encapsulated behind a safe API. This is precisely the
goal of the RustBelt project. Using the Iris logic to encode safety contracts
Semantic type soundness is an old technique, dating back at
RustBelt: Securing the Foundations of Rust least to Milner’s seminal 1978 paper on type soundness [28],
The key challenge in verifying Rust’s safety claims is ac- but scaling it up to realistic modern languages like Rust
counting for the interaction between safe and unsafe code. has proven a difficult challenge. In fact, scaling it up to
To see why this is challenging, let us briefly take a look at languages with mutable state and higher-order functions
the standard technique for verifying safety of programming remained an open problem until the development of “step-
languages—the so called syntactic approach [37, 14]. Using indexed Kripke logical relations” (SKLR) models [5, 3] as
that technique, safety is expressed in terms of a syntactic part of the Foundational Proof-Carrying Code project [4, 2]
typing judgment, which gives a formal account of the type in the early 2000s. Even then, verifications of safety contracts
checker in terms of a number of mathematical inference rules. that were encoded directly using SKLR models turned out to
Theorem 1 (Syntactic type soundness). If a program e is be very tedious, low-level, and difficult to maintain.
well-typed w.r.t. the syntactic typing judgment, then e is safe. In RustBelt we build upon more recent work on Iris [21,
19, 23, 20], a verification framework for higher-order, con-
Unfortunately, this theorem is too weak for our purposes, current, imperative programs, implemented in the Coq proof
because it only talks about syntactically safe programs, thus assistant [1]. Iris provides a much higher-level language for
ruling out programs that use unsafe code. For example, encoding and working with SKLR models, thus enabling us
if true { e } else { crash() } is not syntactically to scale such models to handle a language as sophisticated as
well-typed, but it is still safe since crash() is never executed. Rust. In particular, Iris is based on separation logic [29, 30],
an extension of Hoare logic [15] geared specifically toward
The key solution: Semantic type soundness
modular reasoning about pointer-manipulating programs, and
To account for the interaction between safe and unsafe code, centered around the concept of ownership. This provides us
we instead use a technique called semantic type soundness, with an ideal language in which to model the semantics of
which expresses safety in terms of the “behavior” of the ownership types in Rust.
program rather than a fixed set of inference rules. The key Iris extends traditional separation logic with several addi-
ingredient of semantic soundness is a logical relation, which tional features that are crucial for modeling Rust:
assigns a safety contract to each API. It expresses that if the
• Iris supports user-defined ghost state: the ability to define
inputs to each method in the API conform to their specified
types, then so do the outputs. Using techniques from formal custom logical resources that are useful for proving cor-
verification, one can then prove that an implementation of the rectness of a program but do not correspond directly to
API satisfies the assigned safety contract: anything in its physical state. Iris’s user-defined ghost state
has enabled us to verify the soundness of libraries like
Arc, for which ownership does not correspond to physical
∀Σ.
ownership (e.g., two separately-owned Arc<T>’s may be
∃Φ. . . .  backed by the same underlying memory)—a phenomenon
Logical Formal
relation Safety verification known as “fictional separation” [11, 10]. It has also en-
API Code abled us to reason about Rust’s borrowing and lifetimes
contract
at a much higher level of abstraction, by deriving (within
Semantic type soundness is ideal for reasoning about Iris) a new, domain-specific “lifetime logic”.
programs that use a combination of safe and unsafe code. For • Iris supports impredicative invariants: invariants on the
any library that uses unsafe code (such as Arc, Mutex, Rc, program state that may refer cyclically to the existence
and Vec) one has to prove by hand that the implementation of other invariants [32]. Impredicative invariants play an
satisfies the safety contract. For example: essential role in modeling central type system features
Theorem 2. Arc satisfies its safety contract. such as recursive types and closures.
For safe pieces of a program, the verification is automatic. The complexity of Rust demands that our semantic sound-
This is expressed by the following theorem, which says that ness proofs be machine-checked, as it would be too tedious
if a component is written in the safe fragment of Rust, it and error-prone to do proofs by hand. Fortunately, Iris comes
satisfies its safety contract by construction. with a rich set of separation-logic tactics, which are patterned

7 2021/2/22
after standard Coq tactics and thus make it possible to interac- [6] V. Astrauskas, P. Müller, F. Poli, and A. J. Summers. Leveraging
tively develop machine-checked semantic soundness proofs Rust types for modular specification and verification. PACMPL,
3(OOPSLA), 2019.
in a time-tested style familiar to Coq users [24, 22].
[7] A. Ben-Yehuda. Coherence can be bypassed by an indirect impl
for a trait object, 2019. [Link]
Conclusion and Outlook issues/57893.
In this article we have given a bird’s-eye view of Rust, demon- [8] A. Burch. Using Rust in Windows, 2019. Blog post. https:
//[Link]/2019/11/07/using-rust-in-
strating its core concepts like borrowing, lifetimes, and un- windows/.
safe code encapsulated inside safe APIs. These features have
[9] H.-H. Dang, J.-H. Jourdan, J.-O. Kaiser, and D. Dreyer. RustBelt
helped Rust become the first industry-supported language to meets relaxed memory. PACMPL, 4(POPL), 2020.
overcome the tradeoff between safety and control. [10] T. Dinsdale-Young, M. Dodds, P. Gardner, M. J. Parkinson, and
To formally investigate Rust’s safety claims, we described V. Vafeiadis. Concurrent abstract predicates. In ECOOP, 2010.
the proof technique of semantic type soundness, which has [11] T. Dinsdale-Young, P. Gardner, and M. J. Wheelhouse. Abstraction
enabled us to begin building a rigorous foundation for Rust and refinement for local reasoning. In VSTTE, 2010.
in the RustBelt project. For more details about Rust and [12] M. Doko and V. Vafeiadis. Tackling real-life relaxed concurrency with
RustBelt, we refer the interested reader to our POPL’18 FSL++. In ESOP, volume 10201 of LNCS, 2017.
paper [18] and the first author’s PhD thesis [17]. [13] D. Grossman, G. Morrisett, T. Jim, M. Hicks, Y. Wang, and J. Cheney.
There is still much work left to do. Although RustBelt has Region-based memory management in Cyclone. In PLDI, 2002.
recently been extended to account for the relaxed-memory [14] R. Harper. Practical Foundations for Programming Languages (2nd
concurrency model that Rust inherits from C++ [9], there Edition). Cambridge University Press, 2016.
are a number of other Rust features and APIs that it does [15] C. A. R. Hoare. An axiomatic basis for computer programming.
not yet cover, such as its “trait” system, which is complex CACM, 12(10), 1969.
enough to have been the source of subtle soundness bugs [7]. [16] D. Hosfelt. Implications of rewriting a browser component in
Rust, 2019. Blog post. [Link]
Moreover, although verifying the soundness of an internally- rewriting-a-browser-component-in-rust/.
unsafe Rust library requires, at present, a deep background
[17] R. Jung. Understanding and Evolving the Rust Programming Lan-
in formal semantics, we hope to eventually develop formal guage. PhD thesis, Universität des Saarlandes, 2020. https:
methods that can be put directly in the hands of programmers. //[Link]/~jung/[Link].
Finally, while RustBelt has focused on building founda- [18] R. Jung, J.-H. Jourdan, R. Krebbers, and D. Dreyer. RustBelt: Securing
tions for Rust itself, we are pleased to see other research the foundations of the Rust programming language. PACMPL,
projects (notably Prusti [6] and RustHorn [27]) beginning to 2(POPL), 2018.
explore an exciting, orthogonal direction: namely, the poten- [19] R. Jung, R. Krebbers, L. Birkedal, and D. Dreyer. Higher-order ghost
state. In ICFP, 2016.
tial for Rust’s strong type system to serve as a powerful tool
in simplifying the formal verification of systems code. [20] R. Jung, R. Krebbers, J.-H. Jourdan, A. Bizjak, L. Birkedal, and
D. Dreyer. Iris from the ground up: A modular foundation for higher-
order concurrent separation logic. JFP, 28, 2018.
Acknowledgments [21] R. Jung, D. Swasey, F. Sieczkowski, K. Svendsen, A. Turon,
L. Birkedal, and D. Dreyer. Iris: Monoids and invariants as an
We wish to thank the Rust community in general, and Aaron orthogonal basis for concurrent reasoning. In POPL, 2015.
Turon and Niko Matsakis in particular, for their feedback and [22] R. Krebbers, J.-H. Jourdan, R. Jung, J. Tassarotti, J.-O. Kaiser,
countless helpful discussions. This research was supported A. Timany, A. Charguéraud, and D. Dreyer. MoSeL: A general,
in part by a European Research Council (ERC) Consolidator extensible modal framework for interactive proofs in separation logic.
PACMPL, 2(ICFP), 2018.
Grant for the project "RustBelt", funded under the Euro-
[23] R. Krebbers, R. Jung, A. Bizjak, J. Jourdan, D. Dreyer, and L. Birkedal.
pean Union’s Horizon 2020 Framework Programme (grant
The essence of higher-order concurrent separation logic. In ESOP,
agreement no. 683289), and by the Dutch Research Council 2017.
(NWO), project [Link].192.259. [24] R. Krebbers, A. Timany, and L. Birkedal. Interactive proofs in higher-
order concurrent separation logic. In POPL, 2017.
[25] R. Levick. Why Rust for safe systems programming, 2019. Blog
References post. [Link]
[1] The Coq proof assistant, 2019. [Link] rust-for-safe-systems-programming/.
[2] A. Ahmed, A. W. Appel, C. D. Richards, K. N. Swadi, G. Tan, and [26] N. Matsakis and A. Turon. Rust in 2016, 2015. Blog post. https:
D. C. Wang. Semantic foundations for typed assembly languages. //[Link]/2015/08/14/[Link].
TOPLAS, 32(3), 2010.
[27] Y. Matsushita, T. Tsukada, and N. Kobayashi. RustHorn: CHC-based
[3] A. J. Ahmed. Semantics of types for mutable state. PhD thesis, verification for Rust programs. In ESOP, 2020.
Princeton University, 2004.
[28] R. Milner. A theory of type polymorphism in programming. Journal
[4] A. W. Appel. Foundational proof-carrying code. In LICS, 2001. of Computer and System Sciences, 17(3), 1978.
[5] A. W. Appel and D. McAllester. An indexed model of recursive types [29] P. W. O’Hearn, J. C. Reynolds, and H. Yang. Local reasoning about
for foundational proof-carrying code. TOPLAS, 23(5), 2001. programs that alter data structures. In CSL, 2001.

8 2021/2/22
[30] P. W. O’Hearn. Resources, concurrency, and local reasoning. Theoret-
ical Computer Science, 375(1-3), 2007.
[31] B. Stroustrup. The C++ Programming Language. Addison-Wesley,
2013.
[32] K. Svendsen and L. Birkedal. Impredicative concurrent abstract
predicates. In ESOP, 2014.
[33] G. Thomas. A proactive approach to more secure code, 2019.
Blog post. [Link]
a-proactive-approach-to-more-secure-code/.
[34] M. Tofte and J. Talpin. Region-based memory management. Informa-
tion and Computation, 132(2), 1997.
[35] T. Tu, X. Liu, L. Song, and Y. Zhang. Understanding real-world
concurrency bugs in Go. In ASPLOS, 2019.
[36] D. Walker. Substructural type systems. In B. C. Pierce, editor,
Advanced Topics in Types and Programming Languages. MIT Press,
2005.
[37] A. K. Wright and M. Felleisen. A syntactic approach to type soundness.
Information and Computation, 115(1), 1994.

9 2021/2/22

You might also like