Hacker News new | ask | show | jobs
by jpcfl 845 days ago
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 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.

3 comments

The language runtime can require that the OS & hardware always results in an exception on stack overflow (or, alternatively, compile in explicit checks for it). You running the program in an environment without that is, technically, just as wrong as running it on a system where integer addition does multiplication.

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

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

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

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

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

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

Report it.