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 by the language not to cause any Undefined Behavior for any input.
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, 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.
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, 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
Could we do better? Yes, obviously. We can do a better job of defining Undefined Behavior, 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.)