Live data from Hacker News

Why do we need for an Undefined Behavior Annex to C++

community.intel.com

61–70 of 76 posts

Re: Why do we need for an Undefined Behavior Annex to C++

#61
post #36

Earlier quoted context omitted.

The fact that this program results in reading/writing an unmapped memory address means it’s doing an out-of-bounds access. It segfaults on macOS because the runtime/OS has allocated the stack such that the overflow results in a bad memory access, but that is a behavior of the runtime/OS/hardware, not the language. I guarantee I could exploit this on a system that does not have virtual memory, or a runtime that does n…

> It segfaults on macOS because the runtime/OS has allocated the stack such that the overflow results in a bad memory access, but that is a behavior of the runtime/OS/hardware, not the language. 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 hav…

> Stack overflows are checked in C on macOS not because of guard pages but because the compiler emits stack checks (with cookies).

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?

Re: Why do we need for an Undefined Behavior Annex to C++

#62

Earlier quoted context omitted.

> they aren't messy, they're fully defined. Integer overflow in safe Rust works just like Java: the numbers wrap around. 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…

> No integers that I learned about in math class work like that. 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 t…

> Did you learn about modular arithmetic in math class?

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.

Re: Why do we need for an Undefined Behavior Annex to C++

#63
post #61

Earlier quoted context omitted.

> It segfaults on macOS because the runtime/OS has allocated the stack such that the overflow results in a bad memory access, but that is a behavior of the runtime/OS/hardware, not the language. 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 hav…

> Stack overflows are checked in C on macOS not because of guard pages but because the compiler emits stack checks (with cookies). 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?

> Compiler-emitted stack checking is optional and not the default, and definitely not what is causing the crash here.

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.

Re: Why do we need for an Undefined Behavior Annex to C++

#64
post #61

Earlier quoted context omitted.

> Stack overflows are checked in C on macOS not because of guard pages but because the compiler emits stack checks (with cookies). 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?

> Compiler-emitted stack checking is optional and not the default, and definitely not what is causing the crash here. 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.

Software stack checking does not guarantee protection from stack overflows wreaking havoc. E.g., your thread could blow its stack, then get preempted before the stack checker can run.

Mandating guard pages/MPU protection would rule out targeting embedded platforms which lack sufficient hardware support.

Re: Why do we need for an Undefined Behavior Annex to C++

#65
post #64

Earlier quoted context omitted.

> Compiler-emitted stack checking is optional and not the default, and definitely not what is causing the crash here. 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.

Software stack checking does not guarantee protection from stack overflows wreaking havoc. E.g., your thread could blow its stack, then get preempted before the stack checker can run. Mandating guard pages/MPU protection would rule out targeting embedded platforms which lack sufficient hardware support.

What does preemption change here? Before the stack checker has finished, nothing else should hold a reference to any of the yet-unchecked stack. That's plenty trivial to ensure. (unless you mean preemption somehow breaking the stack checker itself, in which case, well, that's a broken stack checker and/or preemption, and should be fixed)

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).

Re: Why do we need for an Undefined Behavior Annex to C++

#66
post #64

Earlier quoted context omitted.

> Compiler-emitted stack checking is optional and not the default, and definitely not what is causing the crash here. 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.

Software stack checking does not guarantee protection from stack overflows wreaking havoc. E.g., your thread could blow its stack, then get preempted before the stack checker can run. Mandating guard pages/MPU protection would rule out targeting embedded platforms which lack sufficient hardware support.

> Software stack checking does not guarantee protection from stack overflows wreaking havoc.

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.

Re: Why do we need for an Undefined Behavior Annex to C++

#67
post #64

Earlier quoted context omitted.

Software stack checking does not guarantee protection from stack overflows wreaking havoc. E.g., your thread could blow its stack, then get preempted before the stack checker can run. Mandating guard pages/MPU protection would rule out targeting embedded platforms which lack sufficient hardware support.

> Software stack checking does not guarantee protection from stack overflows wreaking havoc. 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 ca…

> Yes it does, unless you're violating the memory model.

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.

Re: Why do we need for an Undefined Behavior Annex to C++

#68
post #47
post #35

Earlier quoted context omitted.

How does Rust implement this on targets where LLVM does not implement stack clash protection?

On Unix targets it installs a signal handler for SIGSEGV and checks if the faulting address falls within the range of the stack guards. See https://github.com/rust-lang/rust/blob/411f34b/library/std/s... 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…

I don't see where those methods are getting called from a Unix signal handler but the code is complex enough that it's easy to miss, especially perusing through github instead of vscode.

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.

Re: Why do we need for an Undefined Behavior Annex to C++

#69
post #65
post #64

Earlier quoted context omitted.

Software stack checking does not guarantee protection from stack overflows wreaking havoc. E.g., your thread could blow its stack, then get preempted before the stack checker can run. Mandating guard pages/MPU protection would rule out targeting embedded platforms which lack sufficient hardware support.

What does preemption change here? Before the stack checker has finished, nothing else should hold a reference to any of the yet-unchecked stack. That's plenty trivial to ensure. (unless you mean preemption somehow breaking the stack checker itself, in which case, well, that's a broken stack checker and/or preemption, and should be fixed) If you can't have hardware support, it's trivial for the compiler to do it in so…

> What does preemption change here? Before the stack checker has finished, nothing else should hold a reference to any of the yet-unchecked stack.

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

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.*

Re: Why do we need for an Undefined Behavior Annex to C++

#70
post #69
post #65

Earlier quoted context omitted.

What does preemption change here? Before the stack checker has finished, nothing else should hold a reference to any of the yet-unchecked stack. That's plenty trivial to ensure. (unless you mean preemption somehow breaking the stack checker itself, in which case, well, that's a broken stack checker and/or preemption, and should be fixed) If you can't have hardware support, it's trivial for the compiler to do it in so…

> What does preemption change here? Before the stack checker has finished, nothing else should hold a reference to any of the yet-unchecked stack. 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,…

Canaries are a separate unrelated thing solving a different problem - buffer overruns, i.e. writing out-of-bounds. (canaries are a best-effort thing and don't guarantee catching all such problems, and they're also useless for safe Rust where unchecked OOB indexing is not a thing; whereas stack overflow checking can be done precisely)

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 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).

Post reply on HN