The current status quo is not the best.
> I could imagine a world where we define annotations around “signed integer will never overflow” that you can add to hit paths but otherwise disallow optimizing around that UB
That's kind of what Rust does - overflow is not UB, but you can use unchecked_add/mul/div/sub to get UB-on-overflow if you explicitly want it.
If you use get_unchecked() to intentionally bypass those bounds checks, the assert is indeed removed, but that requires an unsafe block.
Speaking more generally, Rust gates UB behavior behind unsafe blocks, so it's much harder to unintentionally hit UB than in C.
If the compiler can elide bounds checks in inner loops, that's usually enough. Bounds checks elsewhere rarely affect performance.
This is in contrast to C, where the out of bounds access us merely undefined, so the compiler is allowed to have the program continue execution passed it.
It's also got a great ecosystem. I love the assume crate to do these annotations instead of writing unsafe code explicitly:
assume!(unsafe: i < v.len())
Now you've explicitly written an assumption that will cause the compiler to elide the bounds check in release mode but still assert it in debug mode (vs just doing unsafe & using variants that bypass the bounds check).Indeed, hence "at a minimum". The type system and choosing to avoid UB when defining some operations helps a lot as well, but those have no direct overhead so there's no runtime cost there. Bounds checks (and overflow checks, if those are ever added to release mode) are just the one thing that have a direct runtime cost when they aren't elided.
Safe Rust aims for 0 UB, but I don't think you can make the claim that it absolutely has no UB.
This program SEGFAULTs on my system (macOS), because it's reading an invalid memory address due to a stack overflow:
const N: usize = 1024*1024*1024;
fn main() {
let var: [u8; N] = [0; N];
println!("var: {:?}", var);
}Rust's semantics are to abort on a stack overflow. A language like C or C++ have no such semantics, they may abort or they may continue running and producing jibberish.
EDIT: Going to take it back. I'm unable to create a situation where I create a large stack array that doesn't result in an immediate stack overflow. I even tried nightly MaybeUninit::uninit_array but that crashed explicitly with a "fatal runtime error: stack overflow" so it seems like the standard library has improved reporting instead of the old SEGFAULT. So no UB.
An out of bounds access in Rust will result in a panic but a stack overflow is an abort.
The stack guards would normally be setup by the system runtime (e.g. kernel in the case of the main thread stack, libc for thread stacks), not Rust's runtime. Likewise, stack probes that ensure stack operations don't skip guard pages are usually (always?) emitted by the compiler backend (e.g. GCC, LLVM), not Rust's instrumentation, per se.
In this sense Rust isn't doing anything different than any other typical C or C++ binary, except that automagically hijacking SIGSEGV (or any other signal) from non-application code as Rust does is normally frowned upon, especially when it's merely for aesthetics--i.e. printing a pretty message in-process before dying. Also, attempting to introspect current thread metadata from a signal handler gives me pause. I'm not familiar enough with Rust to track down the underlying implementation code. I presume it's using some POSIX threads interfaces, but POSIX threads interfaces aren't async-signal safe, and though SIGSEGV would normally be sent synchronously (sometimes permitting greater assumptions about the state of the thread), that doesn't mean the Rust runtime isn't technically relying on undefined behavior.
EDIT: To get the guard page range it's using pthread_self, pthread_getattr_np, pthread_attr_getstack, and friends, of which only pthread_self is async-signal safe. See https://github.com/rust-lang/rust/blob/411f34b/library/std/s... I have no concrete evidence to believe the reliance isn't safe in practice on the targeted platforms (OTOH, I could imagine the opposite), but it's a little ironic that it's depending on undefined behavior.
“ //! Finally it's worth noting that at the time of this writing LLVM only has //! support for stack probes on x86 and x86_64. There's no support for stack //! probes on any other architecture like ARM or PowerPC64. LLVM I'm sure would //! be more than welcome to accept such a change! ”
https://github.com/rust-lang/compiler-builtins/blob/master/s...
AFAICT those methods are called from `guard::current`. In turn, `guard::current` is used to initialize TLS data when a thread is spawned before a signal is generated (& right after the signal handler is installed): https://github.com/rust-lang/rust/blob/26907374b9478d84d766a...
It doesn't look like there's any UB behavior being relied upon but I could very easily be misreading. If I missed it, please give me some more pointers cause this should be a github issue if it's the case - calling non async-safe methods from a signal handler typically can result in a deadlock which is no bueno.
I guarantee I could exploit this on a system that does not have virtual memory, or a runtime that does not have unmapped addresses at the end of the stack, to, say, manipulate the contents of another thread’s stack. Therefore, this behavior is undefined.
Now perhaps this means that there are real rust deployments that are "wrong", but that shouldn't include regular sane standard systems, and embedded users should know the tradeoffs.
https://godbolt.org/z/Y75KTT87M:
.LBB3_1:
sub rsp, 4096
mov qword ptr [rsp], 0
cmp rsp, r11
jne .LBB3_1
That's a loop at the start of your 'main' that probes the stack specifically to ensure a segfault definitely happens if your array didn't fit on the stack.Stack overflows are checked in C on macOS not because of guard pages but because the compiler emits stack checks (with cookies). Probably the same is true here.
> I guarantee I could exploit this on a system that does not have virtual memory, or a runtime that does not have unmapped addresses at the end of the stack, to, say, manipulate the contents of another thread’s stack. Therefore, this behavior is undefined.
That's implementation-defined, not undefined.
Compiler-emitted stack checking is optional and not the default, and definitely not what is causing the crash here.
> That's implementation-defined, not undefined.
How could an implementation reasonably define the behavior for a stack overflow that silently corrupts another variable?
It is the default on macOS for clang.
> How could an implementation reasonably define the behavior for a stack overflow that silently corrupts another variable?
Mandate stack checking.
Mandating guard pages/MPU protection would rule out targeting embedded platforms which lack sufficient hardware support.
If you can't have hardware support, it's trivial for the compiler to do it in software - just an "if (stack_curr - stack_end < desired_size) abort();". I can't imagine a platform where there you cannot reasonably get a lower bound for the range of stack available. Worst-case, you ditch the architectural stack pointer and manage your own stack on the heap, if that's what you need to ensure correct Rust behavior on your funky platform (or accept the non-compliant compromise of no stack checking).
If your thread overflows the stack, it could start writing into memory for which it does not hold a reference. If the thread is preempted before the stack checker can run (see below*) and detect the overflow, and another thread runs which accesses the now-corrupted memory, then you're hosed.
> just an "if (stack_curr - stack_end < desired_size) abort();"
That's not how the compiler-emitted stack checking works AFAIK (*I believe it uses canaries on the stack which are checked at certain points in code). But, I could see this solving the problem. Basically, for every instruction that manipulates the stack pointer (function calls, alloca's, and on some arch's interrupts use the current stack), the resulting address would need to be checked. That would be costly and require OS awareness, but I think it would be safe. Is this an option that the compiler provides? It would save me a lot of time debugging.*
In my sibling comment showing the assembly that your Rust program generates, it is writing a "0" every 4096 bytes of the stack range that is intended to be later used as the buffer (this "0" is independent from the "0" in your "[0; N]"; it's just an arbitrary value to ensure that the page is writable). It does this, once, at the very start of the function, before everything else (i.e. before the variable "var" even exists, much less is accessible by anything or even initialized). This is effectively exactly the same as my "if (stack_curr - stack_end < desired_size) abort();", just implemented via guaranteed page faults. You can enable this on clang & gcc with -fstack-clash-protection where supported.
Indeed, stack checking can have overhead (so do other requirements Rust makes!), but in general it's not that large. If you don't have stack-allocated VLAs, it's a constant amount of machine code at the start of every function, checking that all possible stack usage the function may do is accessible. And on systems with guard pages (i.e. all of non-embedded) the overhead is trivially none for functions with frame size below 4096 bytes (or however big the guard range is; and for larger frame sizes the overhead of this check will be miniscule compared to whatever actually uses the massive amount of stack).
Yes it does, unless you're violating the memory model. Or are you thinking of Unix signals? Those do seem a bit harder to implement perfectly.
> Mandating guard pages/MPU protection would rule out targeting embedded platforms which lack sufficient hardware support.
Such systems are not secure if they don't have IOMMUs. But can always emulate everything in software and you must do so here.
Overflowing the stack violates the memory model.
> Such systems are not secure if they don't have IOMMUs.
Secure in what sense? I was under the impression that Rust could run on embedded devices like the ARM Cortex-M3, but maybe I'm wrong.
It's not possible on all platforms, hence the tiers.
Apparently ARM64 macOS has tier 2 rust platform support, which might mean that that this is not true there, but maybe safe rust has some different unrelated soundness issue on this platform.
I only have very surface knowledge about the tier stuff, so maybe someone can correct me.
No, they aren't messy, they're fully defined. Integer overflow in safe Rust works just like Java: the numbers wrap around. It's totally defined.
You can, optionally, also enable a runtime check for this wraparound. This is usually enabled for debug builds. There is of course a performance cost to doing this.
That is both fully defined and messy. They are not mutually exclusive. It means there are integers where n+1 is less than n. That is messy. No integers that I learned about in math class work like that.
The only two non-messy well-defined behaviours are 1) bignums by default like in Python, but not suitable for a low level language like Rust; or, 2) trap on overflow, like Ada is supposed to do though it is usually shut off by a pragma. Both of those have significant runtime cost.
Did you learn about modular arithmetic in math class?
> The only two non-messy well-defined behaviours are 1) bignums by default like in Python, but not suitable for a low level language like Rust; or, 2) trap on overflow, like Ada is supposed to do though it is usually shut off by a pragma. Both of those have significant runtime cost.
No, they're not the only "non-messy" behaviors. An example of where wraparound is the desired behavior is computing hash functions. An example of where saturating arithmetic is the desired behavior is processing audio samples.
I knew some wiseacre would say that. No those aren't integers, they are equivalence classes of integers.
Yes there are times when wraparound is desirable, just like there are when addition mod 12 is desirable (when figuring out times of day). But you don't want the default behaviour of integers to give 8+5=1. If you want that, fine, but ask for it explicitly.
For example, in Ada, if you want modular arithmetic, just specify it in the variable declaration. That is the right way to do it. C++ gets it wrong in that unsigned overflow is modular, signed overflow is UB (so at least you can ask the compiler to signal an exception), but there is no option of unsigned arithmetic where overflow is an exception.
And they form a ring and sometimes even a field. That's the least messy behavior of any arithmetic type usually implemented on a computer. The only messy part is division, which doesn't match modular division, but it's what you actually want, usually.
Most of the design problems around arithmetic types in programming languages are a result of people "wanting integers" (or "real numbers") rather than facing the reality of the machines they're programming for.
> trap on overflow, like Ada is supposed to do though it is usually shut off by a pragma
Sounds like people "usually" opt for "messy" behavior.
> For example, in Ada, if you want modular arithmetic, just specify it in the variable declaration. That is the right way to do it.
No disagreement there. Unfortunately, almost all non-niche programming languages get arithmetic wrong, ironically, because they're supposed to be the simplest types.
You have to choose from a range of possible behaviors that all have their use cases, and you have to be aware of the limitations of the actual type you're working with, which is not an integer but something finite. The language cannot make that choice for use. What the default is almost doesn't matter because choosing a particular behavior should be clear and simple.
Yes, modular arithmetic is convenient for computers, but it's not integer arithmetic! If you're using machine words to denote actual integers, and your program does something that causes an overflow, that is an error and signalling the error is far better than quietly giving the wrong answer. It's just like in Python where ints are arbitrary precision. They can still get too big for the implementation (i.e. the computer can run out of memory) but that is unquestionably an error condition. Machine integers are the same thing except the constraint is running out of bits in the machine word, rather than running out of memory. If that happens, it's also an error, unless you chose a datatype indicating a different intention.
This all seems obvious to me, but I've seen the same misunderstanding in other places before. I don't understand why it isn't obvious to everyone.
> that must be enforced at non-zero runtime cost.
Some of Rust's behavior that permits this is not zero runtime costs. Bounds-checking, for example, has a non-zero cost: the bounds check!
Now, I greatly prefer Rust's approach: I'd rather have the marginal CPU cost: I value my time far higher, at least until the profiler speaks up, and in many cases the optimizer is pretty good at eliminating checks that I myself might look at and go "but I know $condition is true here!" — so too does the optimizer. (And these days, Godbolt makes testing that very simple.)
And in the worse case, there's unsafe{}, and it is at least explicitly labelled as such to the next reader.
> things are still a bit messy with integer overflow
Integer overflow is well-defined behavior, but the behavior depends on compile settings. (https://doc.rust-lang.org/book/ch03-02-data-types.html#integ...) I do hope that someday, the debug behavior becomes the behavior: IME most overflows are errors / the author did not intend for it to occur. And the "panic on overflow" behavior is removable by simply using one of the functions that specifies an overflow behavior, in which case then the author's intention is just explicitly stated.)