10 ms·
> Rust is designed around safety and soundness. Roughly speaking, safe code is code that doesn't use the unsafe keyword, and sound code is code that can't cause
by LegionMammal978 4y ago
> Rust is designed around safety and soundness. Roughly speaking, safe code is code that doesn't use the unsafe keyword, and sound code is code that can't cause memory corruption or other undefined behavior.
I'd be a bit more precise here. Safety is indeed a property of some particular lexical part of the code, but soundness is really a property of an entire interface. A sound interface never causes UB when you invoke it according to its documented preconditions; an unsound interface may possibly cause UB in some situations even when invoked according to its preconditions.
Some take a narrower definition of soundness, treating it primarily as a property of a safe interface: a sound interface that can be invoked from safe code can never cause UB under any circumstances. But personally, I prefer the broader definition, since it also includes unsafe interfaces which don't document all the preconditions they rely on.
Knowing your interface boundary is an important part of writing unsafe code in Rust. For instance, suppose you have a data type with a method that unsafely makes a certain assumption about its fields. Then you have to be very careful to uphold this invariant in every other safe method that creates or modifies those fields. Furthermore, you have to ensure that no language-level operations on the object as a whole (e.g., swapping, dropping, forgetting, or covariant transformations) can break this invariant. But once you have locally checked all of those things, then you don't need to check any other part of the code base to know that your data type is sound.
As another example, contrary to what some people say, it's not necessarily unsound to have a safe function that can cause UB on some inputs. You just have to ensure that external code cannot call that function (without having previously unsafely promised not to call it incorrectly), and no internal code calls it incorrectly. Here, the interface boundary is the entire section of your code that has access to the function.
In particular, you sometimes see this with data types that have to implement safe traits using unsafe operations (for performance or for other reasons). Often, the function to create an object is unsafe, since you have to promise that the rest of your safe code does not invoke those trait implementations in such a way as to cause UB. Practically, you can do this by limiting how much of your code has access to the created object.
- mlindner 4y ago> Knowing your interface boundary is an important part of writing unsafe code in Rust. For instance, suppose you have a data type with a method that unsafely makes a certain assumption about its fields. Then you have to be very careful to uphold this invariant in every other safe method that creates or modifies those fields. Furthermore, you have to ensure that no language-level operations on the object as a whole (e.g., swapping, dropping, forgetting, or covariant transformations) can break this invariant. But once you have locally checked all of those things, then you don't need to check any other part of the code base to know that your data type is sound. This is simply incorrect. If it is possible to, in any safe code, invoke the invariants that can cause the undefined behavior then your code is unsound. UnSafe code _cannot_ rely on safe code invoking it correctly. ALL invariants must be covered at the boundary between safe and unsafe code, anything else is a soundness bug in the code. > As another example, contrary to what some people say, it's not necessarily unsound to have a safe function that can cause UB on some inputs. You just have to ensure that external code cannot call that function (without having previously unsafely promised not to call it incorrectly), and no internal code calls it incorrectly. Here, the interface boundary is the entire section of your code that has access to the function. This is again completely incorrect. If it is possible to invoke unsound behavior from safe code then the code is fundamentally unsound. This is the problem that the Actix developer kept trying to justify that got him in a lot of hot water with the community. Please never write code this way. If you have any public repositories please mention them so bugs can be filed against them. Put another way, a bug in any piece of safe code must never cause any piece of unsafe code to commit undefined behavior. That’s the fundamental guarantee that the entire language is based on. Please don’t violate that by lying to developers that code is safe when it‘a not.
- LegionMammal978 4y ago> This is simply incorrect. If it is possible to, in any safe code, invoke the invariants that can cause the undefined behavior then your code is unsound. UnSafe code _cannot_ rely on safe code invoking it correctly. ALL invariants must be covered at the boundary between safe and unsafe code, anything else is a soundness bug in the code. That's roughly what I'm saying? The point is that you have to carefully design your interface so that safe code from the outside is unable to break your invariants, no matter how it invokes your interface. > This is again completely incorrect. If it is possible to invoke unsound behavior from safe code then the code is fundamentally unsound. This is the problem that the Actix developer kept trying to justify that got him in a lot of hot water with the community. Please never write code this way. If you have any public repositories please mention them so bugs can be filed against them. If you have a public safe function that can cause UB, then it is indeed unsound. But it is possible to have a private safe function that can only possibly be accessed from a small part of your code base. If you are careful in preserving this invariant within the parts of the code that can directly access the function, then creating a sound interface without direct access to the function is entirely possible. (But this obviously gets more risky, the more code that has access to the function. I'll grant that it's not an uncommon pitfall to assume a crate-wide invariant somewhere that you later forget about.)
- codeflo 4y agoYou even have this phenomenon inside single functions. Let's say you write a function that's supposed to be a safe/sound abstraction. You might be setting up a pointer, maybe determined by a dozen lines of codes, that is then finally dereferenced. You'd typically only put the unsafe block around this final pointer access, but its safety depends on the whole calculation being correct.
- zozbot234 4y agoUnsafe blocks are very different from unsafe-marked functions. A function that can indirectly cause UB when called in safe code must be marked as unsafe, so that the invariants it relies on can be properly documented in a Safety: comment, and manually checked by callers.
- humanrebar 4y ago> ...soundness is really a property of an entire interface. Not really. I mean, maybe... if you limit the scope of your consideration to language design itself. If Rust call out to C code and they were using incompatible understanding of an array length due to mismatching build rules (like preprocessor definitions), you have a soundness bug it won't show up obviously in any interfaces anywhere. Soundness is a property of a single build of a program. You cannot generalize it. That is why in safety critical software, you cannot buy "safe" libraries or tools. You can buy libraries and tools with certification packets but context is very important when it comes to program correctness in all its forms.
- oconnor663 4y ago> Soundness is a property of a single build of a program. You cannot generalize it. I think it's important to be able to talk about sound functions, without needing to know anything about their callers, other than what's expressed in the function signature.
- humanrebar 4y agoI'm saying even for that case, you have to know how the implementation of that function was compiled, linked, and possibly even deployed. You cannot promise an interface is "sound". You can promise certain expectations were communicated clearly within some parameters (like object types and lifetimes assuming a coherent build process).
- oconnor663 4y agoI think "soundness" is meaningful and useful in terms of the Abstract Machine that Rust is defined against. Or you know, the one that it will be defined against someday :) It's a way of separating things that are "under the language's control" in a sense from things that aren't. Just to pick an extreme example, if the `add` instruction is broken on my CPU, and it gives the wrong answer 0.001% of the time, and that causes my code to crash, is my code "unsound"? I think soundness/unsoundness just won't be a very useful idea if that's what we mean by it. Of course I do want my program to work, and I do need to fix things one way or another. I'm probably going to download a microcode update, or else throw my CPU in the trash, or possibly even turn on some special compiler workaround that avoids emitting `add`. But I think it's important and interesting to be able to talk about how I'm probably not going to fix my program by changing how I use the addition operator. There's really nothing wrong with my application code, even though my program isn't working. This is the sort of distinction that soundness is trying to draw.
- oconnor663 4y agoI think what you're describing might be similar to footnote/marginnote #6?
- LegionMammal978 4y agoSorry, I was reading the article from my phone and didn't notice that footnote. It definitely ties in to the point I'm making here, but I still feel that the distinction is important enough to elaborate on a bit.
- Animats 4y ago> Then you have to be very careful The problem.
- LegionMammal978 4y agoWell, in principle, you could place each field in its own wrapper type that is unsafe to access. But you'd still have to be nearly as careful: there's no way around that, short of formal verification. Rust is ultimately a pragmatic language, and it lets the user pick their preferred balance between decreased performance, extra care, or extra line noise that people will get tired of reading. At best, it can give suggestions in the design of its standard library of where that balance ought to be.