Search code examples
c++ucontext

Considering makecontext() what is uc_stack.ss_size good for?


Prior to calling makecontext why do we need to set the stack size ss_size?

I just had an unit test case for makecontext/swapcontext snippet and it failed with SIGSEGV. What happened was that stack size was too small and unrelated memory (happened to be some unique pointers) got corrupted and reported segfault. So the segfault was on these unrelated pointers, I could have had e.g. some string and then the memory corruption would have been unnoticed.

I would have expected that SIGSEGV is beeing raised immediately when stack size ss_size does not suffice, but considering the memory corruption described above, I conclude its impossible to recover from SIGSEGV here. That brings me back to the question, why do we need to set the stack size then in first place, when it is not being used to signal overflows? What is it used for?


EDIT:

Well it's all about makecontext(3). These functions are still being used for green threads, coroutines etc. There is just no real replacement for them considering these tasks (in my opinion) also not in c++.

ss_size defined in sigaltstack(2) is being needed for uc_stack in ucontext_t defined in getcontext(3).

Following a minimal verifiable example that shows the memory corruption, by "painting" the memory, described above.

#include <iostream>
#include <ucontext.h>
#include <memory>
#include <cstring>
#include <stdio.h>
#include <unistd.h>

ucontext_t caller, callee;
void cb(void){
    //paint stack with 2
    char tmp[7000];
    std::memset(tmp,2,7000);
    //note stack size specified 6k bytes in size
    //this should not be allowed.
    //furthermore there is not even some signal raised here
    //i expected raised SIGSEGV when this call stack exceeds ss_size
    //it makes ss_size useless no?
}
int main(){
    //
    std::memset(&caller,0,sizeof(caller));
    std::memset(&callee,0,sizeof(callee));

    //create stack and paint 0
    std::unique_ptr<std::byte[]> stack(new std::byte[10000]());
    std::memset(stack.get(),0,10000);//paint stack 0

    //make context
    //note stack specified to [2000,8000)
    //that means [0,2000) and [8000,10000) should not be touched
    if(getcontext(&callee) == -1) {std::cout << errno << ":" << std::strerror(errno) << std::endl; return 1;}
    callee.uc_link = &caller;
    callee.uc_stack.ss_sp = stack.get()+2000;
    callee.uc_stack.ss_size = 6000; //what is this line good for, what is it guarding?
    makecontext(&callee,cb,0);

    //swap to callee
    if(swapcontext(&caller,&callee) == -1) {std::cout << errno << ":" << std::strerror(errno) << std::endl; return 1;}

    //print color - should be 0
    //if 2 then memory corrupted by callee
    std::cout << int(stack[996]) << std::endl;
    std::cout << int(stack[997]) << std::endl;
    std::cout << int(stack[998]) << std::endl;
    std::cout << int(stack[999]) << std::endl;
    return 0;
}

Once again what I don't understand is why we need to set the stack size ss_size, because it looks like that it is not being used to guard against memory corruption or anything else. It looks like it is just there to be there but without any use. But I can't believe that it has no use. So what is it "guarding" / good for?

Well, I don't want to bring more confusion into this. The goal is to get away from a fixed size function call stack by either being able to recover by installing SIGSEGV signal handler, but this looks like mission impossible due to this memory corruption; or to have a growable stack e.g. using mmap(2) with MAP_GROWSDOWN flag, but this looks broken and therefore not an option.


Solution

  • callee.uc_stack.ss_size = 6000; // what is this line good for, what is it guarding?

    This line set's the stack size (as you could read in man sigalstack). From reading makecontext from glibc the ss_size is used for determining end of stack, where glibc setups the stack of the new context. Because stack on some machine "grows toward numerically lower addresses" (like it does on x86 architecture and wiki x86) the makecontext needs/wants to place it data on the end of the stack. So it needs to determinate the end of stack and this is what ss_size is used for.

    Setting ss_size to any value does not mean that overflowing the stack size will issue a operating system signal to your process that notifies that your process tried to access restricted memory area. The implementation of *context isn't (and, well, shouldn't be) designed to make the address ss_sp + ss_size (+ 1) as kernel protected memory, so that writing to that address will trigger segmentation fault. This is still all normal variables. As always with writing to an unknown memory location and for example overflowing arrays, the invalid address may just happen to be inside your process address space, so according to the kernel the process will be writing inside it's address space and everything is fine. As you do here - your cb function writes inside new std::byte[10000] memory, from the kernel perspective there is nothing wrong with that.

    You most probably could allocate new std::byte[6000] and run your process under valgrind or gdb or other tools to inspect malicious writes.