How to create stack memory with guard pages?

It is needed to port language runtime with coroutine support. Guard pages are required to handle stack overflow (runtime system can handle stack overflows and resume execution, stack memory is allocated with page probing to prevent bypassing guard page).

Kernel use create_area_etc (usage when creating thread stack), that is not available from userland.

Also it will be nice to have ability to change thread stack area in kernel.

You can just do it “manually”, i.e. by mmap’ing an area before or after the stack area with PROT_NONE, which should work on all POSIX OSes, right?

You can use mprotect() with PROT_NONE to protect a memory page (including one that’s in your stack area) and you can use pthread_attr_setstack to set a manually allocated stack for a specific thread (only when creating it, not after it has started running).

I think it should be possible with these two and mmap to:

  • Reserve some address space for your stack
  • Use pthread_attr_setstack to point the thread to it
  • Use mprotect to setup your guard page
  • Use mmap to populate the stack as needed with actual memory

So, indeed this should be pretty close to what’s available on any POSIX OS.

1 Like

This can’t be used for switching coroutines. Windows has nice API for creating and switching thread stacks: Fibers.

What API will be used for reserving address space?

You can use mmap with appropriate flag:

mmap(0, bytes, PROT_NONE, MAP_NORESERVE | MAP_PRIVATE | MAP_ANON, -1, 0);

This creates an address space reservation that you can latter fill in by calling mmap again with PROT_READ | PROT_WRITE and removing the MAP_NORESERVE flag

Example usage in WebKit:

1 Like

Also what B_STACK_AREA protection means? Is it needed for custom stacks? I tried to set it, it was displayed in SystemManager, but it seems to have no effect.

In src/system/kernel/vm/vm.cpp it seems to be used only for ensuring a minimum size for the area of two pages (the minimum required to have an usable page and a guard page).

It seems to also allow overcommitting.

It is probably best for languages to just implement their own coroutine-switching functions, or use a library like libco for it (one of the backends uses POSIX setjmp/longjmp and thus should be architecture-independent.)

It is currently not possible to change thread stack range (see thread_info stack_base/stack_end) and stack area for running thread. Program may want to exit and delete coroutine that it using default thread stack. Windows fiber API support registering stack virtual memory and stack range in TEB so it behave exactly same as default thread stack and stacks can be deleted including default one.

I tried this approach, separate areas are created for guard pages. This is probably bad because a lot of coroutines can be created.

Test code:

#include <fcntl.h>
#include <sys/mman.h>
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>

#include <OS.h>

struct TryFrame {
	sigjmp_buf context;
	void *trapAdr;
};

static void SignalHandler(int signal, siginfo_t *signalInfo, ucontext_t *ctx, TryFrame *frame)
{
	printf("Signal\n");
	frame->trapAdr = signalInfo->si_addr;
	siglongjmp(frame->context, 1);
}

int main()
{
	size_t size = 16*1024*1024;
	void *mem = mmap(0, size, PROT_NONE, MAP_NORESERVE | MAP_PRIVATE | MAP_ANON, -1, 0);
	printf("mem: %p\n", mem);
	void *allocMem = mmap((void*)((int8*)mem + B_PAGE_SIZE), size - 2*B_PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0);
	printf("allocMem: %p\n", allocMem);

	TryFrame frame = {0};

	struct sigaction action = {0};
	action.sa_handler = (__sighandler_t)SignalHandler;
	action.sa_flags = SA_SIGINFO;
	action.sa_userdata = &frame;
	sigaction(SIGSEGV, &action, NULL);

	if (sigsetjmp(frame.context, true) == 0) {
		for (int8 *ptr = (int8*)mem + size/2; ; ptr--) {
			*ptr = 0xcc;
		}
	} else {
		printf("low: 0x%" B_PRIxADDR "\n", (int8*)frame.trapAdr - (int8*)mem);
	}

	if (sigsetjmp(frame.context, true) == 0) {
		for (int8 *ptr = (int8*)mem + size/2; ; ptr++) {
			*ptr = 0xcc;
		}
	} else {
		printf("high: 0x%" B_PRIxADDR "\n", ((int8*)mem + size) - (int8*)frame.trapAdr);
	}

	printf("[WAIT]"); fgetc(stdin);
	return 0;
}

list_area:

./MMapTest2 (team 56487)
   ID                             name   address     size   alloc. #-cow  #-in #-out
------------------------------------------------------------------------------------
6051848                         rld heap  0x46e32aa000    10000     d000     0     0     0
6051849                      _rld_debug_  0x70f71b5000     1000     1000     0     0     0
6051858              MMapTest2 mmap area  0x8046306000     1000        0     0     0     0
6051859              MMapTest2 mmap area  0x8046307000   ffe000   ffe000     0     0     0
6051860              MMapTest2 mmap area  0x8047305000     1000        0     0     0     0
6051850                 MMapTest2_seg0ro  0xb156819000     1000        0     0     0     0
6051851                 MMapTest2_seg1rw  0xb156a19000     1000     1000     0     0     0
6051846            runtime_loader_seg0ro  0xd440571000    21000        0     0     0     0
6051847            runtime_loader_seg1rw  0xd440791000     2000     2000     0     0     0
6051855             libgcc_s.so.1_seg0ro  0x13753b97000    16000        0     0     0     0
6051856             libgcc_s.so.1_seg1rw  0x13753dac000     1000     1000     0     0     0
6051852                libroot.so_seg0ro  0x141fdabc000   10d000        0     0     0     0
6051853                libroot.so_seg1rw  0x141fddc9000     e000     e000     0     0     0
6051854                libroot.so_seg2rw  0x141fddd7000    44000     6000     0     0     0
6051857                             heap  0x10c4ebaa7000    50000    1b000     0     0     0
6051844            MMapTest2_56487_stack  0x7fd732900000  1006000     6000     0     0     0
6051843                        user area  0x7ff38a47b000     4000     4000     0     0     0
6051845                         commpage  0x7ffff5f4d000     8000        0     0     0     0