"The Stack Clash is a vulnerability in the memory management of several operating systems. (...) It can be exploited by attackers to corrupt memory and execute arbitrary code."
"If you are using Linux, OpenBSD, NetBSD, FreeBSD, or Solaris, on i386 or amd64, you are affected. Other operating systems and architectures may be vulnerable too, but we have not researched any of them yet: please refer to your vendor’s official statement about the Stack Clash for more information."
There is good news, this looks way harder to attack on 64 bit systems (all the CVE are on 32 bit OS), possibly because the adress space is so huge, and standard OS counter measure like ASLR, stack gap etc all help protecting against the attack.
Section IV.1.7 ("64-bit exploitation") mentions CVE-2017-1000379.
The other way around. The kernel has already mapped pages for the stack, and would never hand such pages to malloc unless there's a serious bug in the vm subsystem.
However, code that uses the stack has no idea where the stack ends. And reaching out of the stack is only detected via page faults. If that access happens to land on a mapped page with the right permissions, there is no fault and the code can effectively grow the stack into heap region.
In theory, you can always decrement the stack pointer for your variables. If you find an unallocated page, the kernel will notice that you're right below the stack and give you more stack pages. There's no other to request more stack memory, the way you can use brk() or mmap() to request more heap memory: you're supposed to page-fault and let the kernel come up with more stack.
In slightly less theory, you can decrement the stack pointer, and if you reach more memory than the kernel is willing to give you, the page fault will turn into an actual segfault, because you'll hit a specially-defined guard page that prevents you from infinitely growing the stack.
In practice, you can decrement the stack pointer by any arbitrary amount and now you just have a pointer somewhere and you have to hope it's either within the stack or in the guard page....
Exactly. But the stack is only growing in the program's view of the world.
> So the OS assumes that the stack already owns that memory
Nah, the OS is blissfully unaware that the program has moved its stack pointer to point off the stack. If anything, the OS wrongly assumes the program is still operating within the stack space explicitly and rightly reserved for it.
And the program in turn wrongly assumes this memory newly referenced via the stack pointer has been reserved for its stack, because it wasn't killed for accessing it.
> Now if you write to the heap, you can corrupt the stack.
That is right. You will corrupt what the program believes to be stack.
The user-space stack of a process is automatically expanded by the kernel:
- if the stack-pointer (the esp register, on i386) reaches the start of the stack and the unmapped memory pages below (the stack grows down, on i386),
- then a "page-fault" exception is raised and caught by the kernel,
- and the page-fault handler transparently expands the user-space stack of the process (it decreases the start address of the stack),
- or it terminates the process with a SIGSEGV if the stack expansion fails (for example, if the RLIMIT_STACK is reached).
Unfortunately, this stack expansion mechanism is implicit and fragile: it relies on page-fault exceptions, but if another memory region is mapped directly below the stack, then the stack-pointer can move from the stack into the other memory region without raising a page-fault, and:
- the kernel cannot tell that the process needed more stack memory;
- the process cannot tell that its stack-pointer moved from the stack into another memory region.
The real issue here is that writing programs in memory-unsafe languages is inherently difficult and risky, and fewer programs should be written that way.
It looks like the fix is going to involve adding some code to LLVM to probe each stack page when you make a large stack allocation, but once that happens, it's straightforward for clang to implement -fstack-check for C and C++ programs, too.
This is a weird emergent problem from the fact that stack and heap memory are part of the same address space and that the stack is designed to implicitly grow as needed. It's not clear that it's a language or compiler's fault for relying on the stack doing that, nor that it's the platform or kernel's fault for making that approach possible.
I'm not totally sure how you would design a language so that you don't have this problem. I guess you could forbid a function from using more than some small amount of stack, and make sure your stack guard area is at least that big, but that seems like an unfortunate restriction. Maybe something like the Stackless Python approach, where all variables are heap-allocated, would work?
Also, all that said, note that Windows gets this right: MSVC inserts calls to _chkstk() to do stack probing. I believe this entire class of vulnerability doesn't exist on Windows, and probably some people at MS who spend their entire lives in memory-unsafe languages are feeling very smug today.
At a source level, these programs may as well be 100% bug free.
This is nothing more than a quirk of implementation. And one that affects all languages that use the stack (and by which I mean the stack, not some heap-allocated structure the language provides stack-like operations on). Memory safety doesn't really enter the picture.
If "all programs are safe", we could just use the Amiga OS kernel and no longer need an mmu, or a similar design.
[ed: apparently windows NT takes steps to avoid this according to a sibling comment. Not clear, but I assume it implies a performance hit for certain heavy stack usage?]
If performance were a concern (and I don't really think it is), it should be possible to reduce the impact by assuming a sufficiently large stack guard that the majority of functions with fixed size stack frames could never leap over. Then these functions wouldn't need any runtime checks. The only checking you'd have to do is on code that uses VLAs, alloca, or such, along with the few outlier functions that use ridiculously large fixed size buffers. I don't see why you should need to touch every page.
1. Memory-safe languages are about making sure that the programmer's intended behavior matches the actual behavior, that is, eliminating a class of bugs related to memory unsafety. They are a security scheme insofar as these bugs are security bugs, but they're not an interprocess security scheme. In particular, you can write a memory-safe debugger that goes and makes arbitrary modifications to other processes the OS gives it access to. You can even write a memory-safe program in Rust that goes and edits /proc/self/mem. But in these cases, the programmer is intending to mess with process memory directly, so the language isn't obligated to stop the programmer. It is obligated to stop the programmer from, say, overflowing a string and overwriting the return address.
2. It is certainly possible to design a memory-safe language that is usable for interprocess memory protection. Microsoft had a research OS called Singularity that did exactly this: https://www.microsoft.com/en-us/research/wp-content/uploads/... But it's another step on top of memory safety.
3. Preemptive multitasking and protected memory aren't inherently related (although, yes, in the market, most cooperatively multitasked OSes lacked memory protection, and most OSes with memory protections were preemptively multitasked). You can have a preemptively multitasked system with no MMU at all; you just need to respond to timer interrupts and switch tasks.
I suppose it's fine to say mallloc will return memory, but it's up to the process to check if there are any overlaps - but that sounds a little crazy?
If you decrement your stack pointer by a large value and then offset it - which is essentially what's happening in these cases - the kernel can't arbitrate that because if the access lands in otherwise allocated memory it doesn't fault and so the kernel never sees the access at all.
I can see how the current stack/heap thing evolved - but I still think it's crazy :-)
All that's happening here is that userspace is moving its stack pointer into the heap it had previously allocated. Note that "moving the stack pointer" is not a kernel-mediated operation.
Conversely, if you grow your stack by some value on the order of gigabytes, you're basically coming up with a pointer that appears to have no relation to the stack, and dereferencing it. So the platform is going to do exactly what it does if you were to dereference the same pointer value with no stack involved: read/write memory if it's mapped and segfault if not.
You could totally imagine a platform where growing the stack were a more well-defined operation. You want to avoid each function call and local allocation having the overhead of a system call, though: the nice thing about the current scheme is that it's zero-overhead if there's a mapped stack page. So the scheme was designed (or probably emerged more than was intentionally designed) for the case where syscalls are very slow, MMUs work fine, and perfect memory safety isn't the goal, i.e., the original UNIX target audience. :-)
You could keep a thread-local variable somewhere indicating the current stack limit, and make a system call when you need to increment it. That doesn't require an MMU at all: the userspace API is that you call some system routine when you need to expand the stack, and it says yes or no (or it either says yes or kills you with a segfault, or whatever). In an MMU-less system, you can just keep track of the amount of heap allocation, and have the system routine fail when you're too close to your heap.
Or you could do stack probing, which works but requires an MMU.
These are remarkable because they are conceptually straightforward but the power of the exploits was potentially substantial.
edit: original first sentence was "told Red Hat, Red Hat fixed them." Apparently that was wrong and people (appropriately!) want credit attributed to the right parties.