STACK(7)
STACK(7) is from the Miscellaneous Information Manual, and today we're getting into how the stack memory of your program is laid out. grab a snack bestie π this one's a bop.
β¨ NAME β¨
stack β layout of program execution stack memory π
πΈ DESCRIPTION πΈ
ok so picture this: a program is executing, right β via the execve(2) or posix_spawn(3) families of system calls β and NetBSD reserves a region in the new program image's virtual address space for the stack, which stores return addresses and local variables for nested procedure calls in program execution. literally the whole backbone of procedure calls. π₯
Same energy for threads, btw: threads created with pthread_create(3) have regions allocated for per-thread stacks. π«Ά
The stack grows from the base β the base is where the information of the outermost procedure call is stored, fixed at program start β to the stack pointer, which is a CPU register that points to information used by the current procedure call, and which varies during execution as procedures are called. π±
On most architectures, the stack base is at higher-numbered virtual addresses and the stack pointer is at lower-numbered virtual addresses β on these architectures, the stack grows down. β¬οΈ On some other architectures, notably HP PA-RISC (hppa), the stack base is at lower-numbered virtual addresses and the stack pointer is at higher-numbered virtual addresses, so on those architectures the stack grows up. β¬οΈ
In the kernel, the C preprocessor macro __HAVE_MACHINE_STACK_GROWS_UP is defined in <machine/types.h> on architectures where the stack grows up. noted. π
π Main thread π
For single-threaded programs, and for the main thread of multi-threaded programs, NetBSD reserves virtual addresses as follows on architectures where the stack grows down:
+--------------------+ USRSTACK
| stack gap |
+--------------------+ stack base
| accessible pages |
| . |
| . | <-- stack pointer (varies during execution)
| . |
+--------------------+ (stack base) - (soft stack rlimit)
| inaccessible pages |
+--------------------+ (stack base) - (hard stack rlimit)
| guard/redzone |
+--------------------+ USRSTACK - MAXSSIZ
On architectures where the stack grows up, the layout is:
+--------------------+ USRSTACK + MAXSSIZ
| guard/redzone |
+--------------------+ (stack base) + (hard stack rlimit)
| inaccessible pages |
+--------------------+ (stack base) + (soft stack rlimit)
| . |
| . | <-- stack pointer (varies during execution)
| . |
| accessible pages |
+--------------------+ stack base
| stack gap |
+--------------------+ USRSTACK
ok now let's decode every single one of those regions, no skipping π
- The stack guard is allocated so that any access β read, write, or execute β will deliver
SIGSEGVto the process. This serves to detect stack overflow and crash rather than silently overwrite other memory in the program's virtual address space. The size of the stack guard is tuned by thevm.guard_sizesysctl(7)knob.
The stack guard is also sometimes known as the 'redzone' or 'red zone', although the term 'red zone' is also sometimes used to mean a fixed space above the stack pointer (in the direction of stack growth) that the system guarantees it will not overwrite when calling a signal handler in the ABI of some architectures; see also sigaltstack(2) to specify an alternate stack base for the kernel to use when invoking signal handlers on signal delivery. π¨
- The inaccessible pages of the stack region are allocated so that any access will also deliver
SIGSEGVto the process, but they can be made accessible by changing the soft stack rlimit withsetrlimit(2). π - The accessible pages of the stack region are allocated with read/write access permitted, and are used to store the actual data in the program stack. the real ones. π
- When PaX ASLR, address space layout randomization, is enabled, the stack gap is an unallocated space of a size chosen unpredictably at random at program startup time. When PaX ASLR is disabled, the stack gap is empty. π²
All of the boundaries β USRSTACK, the stack base, and the boundaries between the accessible, inaccessible, and guard pages β are page-aligned, or rounded to be page-aligned even if the rlimits are not themselves page-aligned, rounding so that the sizes of the regions do not exceed the rlimits. everything lines up, it's giving tidy β¨
The stack base is exposed to programs via the AT_STACKBASE elf(5) auxiliary info vector entry. π«Ά
The per-architecture constants USRSTACK and MAXSSIZ are defined in <machine/vmparam.h>.
π¦ Non-main threads π¦
Threads created with pthread_create(3) have stacks allocated at dynamically chosen addresses outside the main thread's stack region by default, and their stacks cannot be resized after creation. on architectures where the stack grows down, the layout is:
+--------------------+ stack base = stackaddr + stacksize + guardsize
| stack |
| . |
| . | <-- stack pointer (varies during execution)
| . |
+--------------------+ stackaddr
| guard/redzone |
+--------------------+ stackaddr - guardsize
On architectures where the stack grows up, the layout is:
+--------------------+ stackaddr + stacksize + guardsize
| guard/redzone |
+--------------------+ stackaddr + stacksize
| . |
| . | <-- stack pointer (varies during execution)
| . |
| stack |
+--------------------+ stack base = stackaddr
The parameters stackaddr, stacksize, and guardsize can be obtained from an existing thread using pthread_getattr_np(3), pthread_attr_getguardsize(3), and the pthread_attr_getstack(3) family of functions. π₯
When creating a thread, the stack can be manually allocated and the parameters can be set using pthread_attr_setguardsize(3) and the pthread_attr_setstack(3) family of functions. However, the stack parameters cannot be changed after thread creation. say it with me: after creation, no take-backs π The default guard size is tuned by the vm.thread_guard_size sysctl(7) knob.
For the main thread, pthread_getattr_np(3) returns a snapshot of the parameters as they existed at program startup, so that stackaddr and stacksize reflect the current accessible pages of the stack, and guardsize is the value of the vm.guard_size sysctl(7) knob at the time of program startup. (Note that this means the pthread(3) view of the main thread's stack guard may not coincide with the actual stack guard β it may overlap with, or lie entirely in, the inaccessible pages of the stack reserved on program start.) However, if the program changes its soft stack rlimit with setrlimit(2), this snapshot may become stale. β³
it's giving "the map is not the territory" and honestly the territory is the inaccessible pages
π SEE ALSO π
execve(2), mmap(2), mprotect(2), sigaltstack(2), ucontext(2), posix_spawn(3), pthread(3), security(7), sysctl(7), paxctl(8)
π BUGS π
PaX ASLR doesn't actually guarantee an accessible stack reservation of length equal to the soft stack rlimit β owing to a bug (XXX which PR number?), NetBSD may sometimes reserve less space than the soft rlimit, in which case the accessible pages of the stack cannot be extended. π not the reservation coming up short
There is a race between the kernel's access of vm.guard_size at exec time, and userland's access of vm.guard_size in pthread(3) initialization. π genuinely a race condition, not the fun kind
and that's the whole page bestie π this one came from the Debian crew, dated November 23, 2023 β ty for the lore, mwah π
π¦ where this page comes from
- nixpkgs unstable: netbsd-source 11.0
- Copyright (c) 2023 The NetBSD Foundation, Inc
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms