I wouldn't say "safe code can cause UB". It's rather one of:
Safe code can be a victim of invalid assumptions or bugs in some unsafe code, and end up being a trigger of UB despite not being at fault (e.g. unsafe code can return &mut to safe code that is non-exclusive or NULL. Safe code using that reference will blow up, but only because it was given an already-broken state).
Safe code may sabotage an unsafe block that relied on the safe code to behave in a certain sensible way.
The second one is trickier to define. For example, if you have an unsafe block that stores a bunch of pointers in a Vec, it will likely depend on the Vec acting sensibly, preserving the order and not shuffling the elements for no reason. If Vec misbehaves, code using it will misbehave too, and that will have worse consequences in unsafe.
In the second case whose fault it is may be very context dependent. In case of trait implementations, unsafe code typically has an obligation n to trust the implementation and has to be coded defensively (e.g. unsafe code must not trust Ord. Some Ord impl might return random ordering, which would be dumb thing to do, and may result in collections heaving messed up content, but it can't be messed up any more seriously than a fully-safe code could mess it up).
In the end, you have to trust that something in the language works as documented. 2+2 is safe, and has to return 4. It would be unproductive to mark it as unsafe and allow it to return 5. It would also be unproductive to say that unsafe code can't rely on 2+2 because add may be buggy.
Thanks for bringing that up. This is a case where safe code triggers undefined behavior while unsafe code causes it. I didn't mention it because I'm only looking at cases where safe code causes undefined behavior. I should probably modify the top post to at least mention that well-documented scenario (and say that this is off-topic).
This is exactly what this thread is about. The whole question being what is "a certain sensible way" (or in technical terms "its safety guarantees")? Currently the convention is that safety guarantees are the logic contract. There is the other extreme where there are no safety guarantees except for the standard library (the Rust Hypothesis). I'm saying crate authors should choose (defaulting to no safety guarantees, which is the safe choice).
Because Vec comes from the standard library, this case is rather simple. It's pretty uncontested that the standard library guarantees for safety that it is correct for logic (which matches the convention).
Indeed, and such context is whether the unit of implementation interacts with a client or a dependency:
If it's a client, Rust is pretty clear that it cannot trust it for logic. So if a client gives you a trait implementation, then you can't trust that implementation to be correct for logic. But you can trust it for safety. This is useless for safe traits because they don't have safety guarantees (which are the safety requirements to implement), but useful for unsafe traits.
If it's a dependency, Rust is pretty unclear. There's this convention that you can trust it for logic. But this contradicts that "safe code cannot cause UB".
Again, everybody agrees on those examples. You can obviously trust for safety that the language and the standard library are correct for logic (their safety guarantees is their logic contract). The question is about all the other crates. Should the answer be the same for all crates? Should the crates be able to decide for themselves? Should the clients of those crates decide instead independently (possibly with differing opinions both among themselves and with the author of the crate)?
The current situation is that the answer is the same for all crates: they all guarantee for safety that they are correct for logic (whether they want it or not). My opinion is that crates should decide for themselves, restoring the statement that "(arbitrary) safe code cannot cause UB" (or equivalently that "safe code can only cause UB within its unit of implementation" or also that "the scope of unsafe is encapsulated within its unit of implementation") which permits auditing crates for safety without looking at how they are used (which is infeasible).
I thought this was the case too, but this discussion is making me change my mind somewhat – it is itself a large dependency that's somewhat difficult to check for correctness.
Rust's standard library contains three different sorts of items:
There are fundamental items that are needed to make Rust work, like references and cells: these are expected to need a lot of unsafe to be implementable just due to their nature, and I have no problem with expecting those to uphold their logic contract. These can be seen as the safe encapsulations around unsafe code that makes various programming techniques possbile.
There are items that are required to allow a Rust program to interact with its surroundings, primarily in std: things like functions for opening files, handling panics, and allocating memory. These are often quite complex and rely in turn on non-Rust dependencies (at some point you have to rely on assembly language or on linking to libraries written in C that are effectively part of the operating system), and they often go wrong in practice in all sorts of ways. These are generally considered out of scope for Rust's soundness/security model, and in general necessarily have to be either dependencies or intrinsics and necessarily have to be trusted, so there's not much you can do about them.
There are also items that exist purely to supply algorithms that could be (and sometimes are) written purely in safe Rust and don't technically need to be in the standard library, but are there to make the language more usable because they are commonly required. For example, f64::sin(), container types other than Box and Vec (which likely fall into the first category), code that performs calculations on dates, Unicode-handling functions, and string-formatting functions. I'm increasingly thinking that it may be wrong to hold these to a higher standard than typical crates providing similar functionality (especially as in some cases, they're just a near-direct copy of a crate from crates.io): they're often quite large and typically haven't been reviewed from the "is this sufficiently correct to use as a dependency of unsafe code?" point of view.
So my view now is more along the lines of "there are some parts of the standard library that it makes sense for unsafe code to rely on for soundness, but this doesn't apply to the whole standard library". In particular, the sort of thing that is both complicated and could in theory be written entirely with safe code, probably a) should be written entirely in safe code, and b) shouldn't be assumed to be logically correct by unsafe code – I don't think it makes particular sense to trust the standard library for this because this sort of code hasn't received any sort of heightened scrutiny (other than its position in the standard library likely making it widely used and putting more eyes on it).
As a concrete example, I would consider it wrong to rely on logical correctness of the trait implementation <f64 as Display> for soundness in unsafe code. For one thing, there's no obvious reason why you would do that, which means that the authors of the display code are unlikely to have had reliability for unsafe code in mind when writing it. (But, in a language other than Rust, I did once end up writing that sort of code – it wanted to allocate a buffer to format a number into, which involved predicting how long it would be when printed, then formatted into the buffer without further size checks. I tested that code extremely heavily but in retrospect it was still a bad idea.) For another thing, this sort of code has historically proven to be extremely difficult to write correctly, often with subtle bugs that take a while to discover (there's an entire blog that contains many examples of float-related bugs, many of which are bugs in the standard library float-formatters of various languages). So there are a lot of risks here, and it would probably be better to use another approach.
I'm actually also of this view. Some items get the full logic contract as safety guarantees (your first category). Some items get no safety guarantees (some of your third category). And the rest gets more custom safety guarantees (like <f64 as Display> could provide an upper bound on the formatted length[1]) depending on what is reasonable to guarantee and potentially useful in practice.
The question is not who gets to decide whether to trust a crate for something, the question is who gets to decide the something of a crate that its clients may decide to trust. Obviously it's the client who must trust its dependencies (and reciprocally). That's how contracts work. It doesn't make sense to trust yourself.
I think you misunderstood the sentence "Should the clients of those crates decide instead independently". The question is about deciding the safety contract, not about whether to trust it or not. This last part is obviously up to the client. Today (both with the convention and with the Rust Hypothesis), the safety guarantees are decided by the language. The safety requirements are also decided by the language for the safe parts of the public API, but they are decided by the crate for the unsafe parts.
that's just a hypothetical example, I'm not suggesting this ↩︎
There is no separate "safety contract", and there is no need for one.
If I write a sort function, what I'm trying to do and what I promise to the users is that the output numbers are the input numbers sorted. There is only one contract here.
Users may trust that promise at different levels:
Trust level
Use the crate
Apply the contractual promise for Safety rationale in unsafe blocks
This is actually a surprisingly interesting example, because sorting functions have a lot of non-obvious guarantees that they can provide (or not) to their callers. Here's a good writeup, which ended up informing the selection of sort algorithms in the Rust standard library, and caused it to include text like this:
Given the context that caused that paragraph to be added to the documentation, I believe that this is intended to be a robustness / reliable-for-unsafe-code guarantee, although that isn't clear from the documentation. It is notable that most sorting algorithms, including many written in Rust, do not uphold these requirements in practice (for example, if <T as Ord>::cmp changes its arguments via interior mutability, many sorting algorithms may sometimes revert the elements to a version from before the change).
I was just thinking about sorting [i32]. Generic sorting with arbitrary T: Ord is an extra complication that is not relevant to the point I was making.
Then what are safety requirements if not (a part of) a safety contract? They obviously differ from logic requirements otherwise all functions would be unsafe, so they obviously are not (part of) the logic contract.
Maybe you don't make a distinction because you don't write logic bugs, in which case the safety contract is indeed useless. But Rust (contrary to C) is supposed to let people write logic bugs without causing undefined behavior in their dependencies. And the topic of this thread is to figure out if Rust is supposed to let people write logic bugs without causing undefined behavior outside their unit of implementation (so both in their dependencies and in their clients).
Safety requirements are simply the part of the (single) contract that says: "if you, the caller, don't satisfy XYZ, the behavior of this function is undefined and all promises are void". My sort([i32]) example has no safety requirements.
This is unproductive terminology bikeshed, right? I hope you see that your single contract is made of a logic part (with requirements and guarantees) and a safety part (with requirements). And I hope you are able to unambiguously map those to "logic contract" and "safety contract", otherwise I'm not sure it's worth continuing to reply to this thread.
Yes. But I wasn't talking about safety requirements, I was talking about promises. My sort example doesn't even have any safety requirements.
Safety requirements are an integral part of the contract. There is only one set of guarantees that the contract has. Safety requirements don't create two versions of the contract, they just provide a boundary to what the contract promises. There are no guarantees at all outside of the boundaries of what safety requirements require. It's just undefined behavior in that scenario.
But what you are suggesting is something completely different. You keep saying that there should be two different versions of the contract. One for purposes of "logic", and a different one for purposes of "safety", with different guarantees. That's what I'm disagreeing with.
…sure, we can consider the whole Tower of Weakenings to be one really complicated contract, or we can consider it to provide multiple contracts. I don’t see why either perspective is objectively wrong.
I see. So in my terms your single contract is the safety contract with the convention. It indeed contains all the information, but doesn't leave room to talk about a language with a different convention. Since you are in favor of the convention, it indeed makes sense to not consider another point of view.
Assuming the convention, the single contract is objectively better, because there's just one contract. (In this case the language only guarantees that "safe code in clients cannot cause UB".)
Discussing alternative conventions (like the Rust Hypothesis or explicit safety guarantees, to try to restore "safe code cannot cause UB outside its unit of implementation"), the 2 contracts is objectively better, because there's no other option.
So without knowing in which of those 2 scenario we are, no perspective is indeed objectively wrong. However, given that this thread is about the language contradiction between the convention and "safe code cannot cause UB", I believe it helps to temporarily (within this thread) use the 2 contracts perspective.
This is incorrect. There is no convention that says that it's always OK to rely on the correctness of the dependencies for soundness purposes. You shouldn't do that if the dependency is not trustworthy, untested, etc. It is your responsibility if you have a soundness bug because you used a buggy dependency.
You seem to be misinterpreting the word "can" which is somewhat ambiguous. Crate authors may (or may not) rely on anything they deem sufficiently reliable (including for soundness purposes), but they are responsible for making that judgement accurately.
For example, you may use Fermat's Last Theorem in your Safety proof. Andrew Wiles doesn't have to declare whether his proof can be used for safety of Rust programs. It is your responsibility to judge whether a proof of a math theorem is solid enough to trust it for soundness of your code.
If you use a shady math theorem that somebody published on the internet, and that theorem turns out to be wrong, and your code unsound as a result, it is your fault.
Same thing for deciding whether to trust dependencies for soundness purposes. There is no convention that absolves you from that responsibility.
This is the distinction between "deciding to trust" and "what it means to trust" as we discussed before. I agree it's subtle, so I'll try again (otherwise it should deserve its own thread). As a crate author you get to choose which dependencies (and which parts of them) you use and how you use them. When you do so, you trust whatever you decided to use to be correct for how you decided to use it (otherwise you wouldn't use it). That's the "deciding to trust" part. Now what it means to be correct is defined by the dependency and the language. That's the "what it means to trust" part. In particular, you don't choose what it means for the dependency to be correct, you only get to choose whether to use it (thus trust it to be correct).
As an example we got earlier. Someone may decide to use the standard library. They may decide to use Box and Vec in unsafe code (thus use/trust the safety contract of those APIs), but use BTreeMap in safe code only (thus its logic contract), since a dependency of safe code being incorrect is not that bad, it can't cause undefined behavior. They did so because they estimated that using BTreeMap in unsafe code is too risky (there's non-negligible chance BTreeMap is incorrect for safety). But they did not decide the logic and safety contracts of the standard library, the standard library and the language did. They just chose what to use and for which purpose based on those contracts.
I'll update the top post because I agree this is subtle and I assumed this implicit aspect was clear. So actually a possible explicit version of the statement provided by the language today is: "safe code cannot cause UB in its dependencies assuming those dependencies are correct for safety" (where the implicit part is in italic). What it means to be correct for safety is defined by the unit of implementation (the safety requirements of their unsafe API) and the language (the safe API must have no safety requirements, and the logic contract is a safety guarantee[1]).
this last part is the topic of this thread and the contradiction is that safe code is not necessarily correct for safety ↩︎
That's perfectly fine, that matches what I'm saying. You are free not to trust BTreeMap enough to use it for soundness purposes, so just don't use it for soundness purposes. You can do that today, no need to change anything in std. There is no need for BTreeMap's interface or documentation to say anything about this. You can already not use it if you don't want to trust it.
BTW, you seem to be confusing "using BTreeMap in your own unsafe code" vs "using unsafe APIs of BTreeMap". These are two separate choices. It's possible to use safe BTreeMap APIs in your own unsafe code.
But either way, you are already free to have any policy you want regarding whether you use BTreeMap for unsafe purposes in your own code. There is no need to change anything in BTreeMap to enable that.
I don't think I do. I'm exclusively talking about the first (since I'm only interested in safety properties of safe code in dependencies). But anyway, I think we agree overall, simply with different words.
Also thanks to your perspective there's a better candidate to what a fully explicit version of "safe code cannot cause UB" could mean (finally got the time to update the top post summary).