In Rust, Undefined Behavior is a specific term. It means undefined in the mathematical sense: an execution of a Rust program by definition performs no undefined operations. If your Rust source describes an undefined operation, then the execution which includes the undefined operation is also undefined in the mathematical sense.
This is what makes Undefined Behavior so problematic. We don't live in a mathematical model, and the Rust compiler functions by producing some bytes which instruct the processer to do some processing. Something occurs if you execute the resulting binary with the correct environment to cause the interpretation of the Rust source to do the undefined operation.
Unspecified, however, only has its informal meaning. But this doesn't mean that a function whose behavior is left unspecified has completely unbounded possibilities — if the function is unsafe
, then fully unspecified behavior includes Undefined Behavior, but a safe function is guaranteed[1] by the language not to cause any Undefined Behavior for any input[2].
The key point of Rust's unsafe
system is encapsulated unsafety. Running completely arbitrary and unspecified Rust code in one crate (in the absence of Undefined Behavior) mathematically cannot impact the execution of a different, unrelated crate. This is the thesis, so a pull quote for emphasis:
Unspecified behavior cannot impact the behavior of code in a separate crate.
Unspecified behavior is unspecified, but it is bounded to operations that the language allows the library to perform. This includes arbitrary global state changes, but only global state that is upstream to the unspecified behavior. Additionally, the privacy barrier of the crate ensures that only the global state of the crate with the unspecified behavior can change in fully unspecified ways — any crates upstream of the unspecified behavior can only be interacted with in a sound manner provided for by its public API. And any crate which is unrelated to the unspecified behavior will not have any global state change.
It is at this point that we need to assume Quality Of Implementation. Because std
is distributed as part of the language, it could have special powers — we must assume that a correct implementation behaves indistinguishably from if it had no special powers. Similarly, because std
is a single crate[3], we must assume that the impacts of unspecified behavior are bound to the object which misbehaves.
It is, ultimately, that last point which is under discussion. It is exactly about providing some bound on the unspecified behavior. The answer, as it is today, is that the unspecified behavior is bound only by Quality Of Implementation.
Although the std
documentation purposely and explicitly leaves the behavior of recursively locking a Mutex
unspecified, the standard library does (informally and implicitly) guarantee a reasonable Quality Of Implementation that behavior is localized.
The Rust project has a very liberal policy that if you think there's a bug, there's almost certainly a bug. It might be considered a documentation bug that the behavior wasn't documented sufficiently, but if the behavior of the standard library surprises a reasonably informed and conscientious Rust developer, that is a bug to be addressed.
This isn't sufficient for a mathematical proof of correct behavior in the face of arbitrary use[4], but is far and beyond sufficient for normal everyday development to assume reasonable QOI from the standard library.
The difference to C++ is that the C++ standard and specification is a formal document. If the standard omits a definition for some behavior, then it is mathematically Undefined Behavior by that omission. Rust does not have an official formal specification at this time. Of the official reference material, the Rustonomicon provides a semiformal description of what Undefined Behavior is in Rust. If behavior is omitted from the standard library documentation, that behavior is merely unspecified by omission — it is a passive guarantee that no Undefined Behavior occurs if you satisfy the documented safety conditions of the unsafe
APIs.
Could we do better? Yes, obviously. We can do a better job of defining Undefined Behavior[5], we can provide a usable definition of the dynamic borrowing rules, we can avoid the term unspecified as too similar to Undefined, we can provide tighter bounds on arbitrary misbehavior where we explicitly allow it, we can do many things. This is mostly a matter of project throughput and figuring out how to structure and deliver this information such that it actually addresses the pain points.
Here's a reasonably short differentiator: If your program manifests arbitrary but defined misbehavior, then you can use Rust tooling to diagnose it. If your program manifests Undefined Behavior, then the Rust tooling can become useless and you may need to diagnose the misbehavior at the next level on the tower of weakenings. (However, due to an immense amount of work, a smattering of good luck, and the practicality of discrete execution rather than pure mathematical models, the tooling built around C/C++ and which extends to Rust will often work well enough even in the face of Undefined Behavior.)
-
This is unfortunately an oversimplification: the actual property which the language guarantees is that all executions do not cause any Undefined Behavior. It is a property of the library that safe functions cannot be used to cause Undefined Behavior.
The definition of the language and the standard library are unfortunately very intertwined. For C and C++, they are quite literally the same thing. For Rust, the standard library gets special permissions not available to other code by virtue of being distributed with and bound to specific distributions of the compiler. ↩︎
-
There are, of course, some caveats. If you never write
unsafe
, none of them apply. The most notable caveat is the concept of safety invariants and the ever-problematic library UB. ↩︎ -
This isn't actually true — the standard library implementation is broken down into (and exposed as) three different crates (
std
,alloc
, andcore
), and internally uses other crates. However, due again to its special privilege, this is all essentially one crate for the purpose of coherence and safety boundaries. ↩︎ -
And basically nobody does that, even softward certification programs. Software certification is typically restricted to proving correctness when used correctly. Rust is unique in providing such strong guarantees in the face of misuse (where the guarantees are possible to break in the first place). ↩︎
-
For better or for worse, Undefined Behavior is the term of art for mathematically undefined behavior which the compiler assumes does not happen; replacing that term would do more harm than good. ↩︎