Skip to content
UNDER THE HOOD

Stack and global objects

Which locals are tracked (RFC 0035 §3.1, amendment 5). A local (an alloca) is tracked when a guard that remains may reach it: any of the objects a guarded pointer may be based on, through phis and selects, is that local, or its address escapes (stored, passed to a call other than a lifetime marker or a memory intrinsic on itself, returned, or converted to an integer). Other locals are left as they are.

The frame (GuardFrames.cpp, RFC 0035 §3.2). The tracked static allocas of the entry block become offsets into one frame alloca, aligned to 32 bytes: a left redzone of 64 bytes, then each object at a 32-byte boundary followed by its redzone, at least 32 bytes and one eighth of the object up to 4 KiB, so the object and its redzone fill a multiple of 32 bytes (guard::paddedSize). At entry the pass writes the frame’s shadow with constant stores (redzones poisoned, partial granules encoded, objects whose scope has not begun 0xF8), and clears it before each return. A lifetime start of a tracked object makes it addressable and a lifetime end poisons it with 0xF8, so a use after scope traps; a use after return is not caught. A function with a musttail call or localescape keeps its locals in place, without redzones.

Shared space (RFC 0036 §2, amendment 8). In a frame that would be larger than 512 bytes, two tracked locals may share bytes when their lifetimes never overlap: LLVM’s StackLifetime analysis, in its may be live mode, over the lifetime markers that remain after the optimiser (the analysis SafeStack uses). Each local is placed at the lowest 32-byte-aligned offset after the left redzone where its object and redzone meet no other local’s that may be live with it, so locals in the disjoint arms of a switch take the space of one, as in Clang’s own frames. A local without lifetime markers (every local at -O0) is live throughout and shares nothing. At entry a shared granule is 0xF8 (out of scope); a lifetime start writes its object addressable, and its redzone 0xF2 again when the redzone overlaps another local’s object, so that an overflow into it reports a stack-buffer-overflow.

Dynamic objects (RFC 0035 §3.4). A tracked dynamic alloca (an alloca call, a VLA, or a static alloca outside the entry block) of n bytes allocates round32(n) + 32 bytes, and its tail is poisoned 0xF4. Before a stack restore and before each return, __weavec_rt_alloca_unpoison(top, bottom) clears the shadow of the dynamic objects made since.

Non-local exits (RFC 0035 §3.5). Before each call of a function that does not return, other than the runtime’s own and those that end the process (abort, exit, the assertion failures, err and the like), the pass calls __weavec_rt_unpoison_stack(), which clears the shadow from the current stack pointer to the thread’s stack base. A longjmp, siglongjmp, pthread_exit, or an interpreter’s error jump written in assembly but declared noreturn then leaves no poison in the frames it abandons. A frame left by code not built by weavec-cc (an exception unwinding through C frames) keeps its poison until the stack is reused (assumption A5).

Globals (GuardGlobals.cpp, RFC 0035 §4). A global the unit defines exactly is tracked unless it is thread-local, in a named section, in a comdat, in a non-default address space, a string literal or other mergeable constant (constant, unnamed_addr and local), common, appending, externally initialised, aligned above 4 KiB, of size 0, or one of LLVM’s or WeaveC’s own. A tracked global is replaced by a global of the same name, linkage and visibility that holds the original and a redzone (sized as a frame object’s), aligned to 32 bytes. A module constructor of priority 1 calls __weavec_rt_globals_register(array, n) with {address, size, size with redzone} per global, which poisons each redzone 0xF9. A shared library’s or bundle’s copy of the runtime sends the registration to the process’s owning runtime first (RFC 0036 §4), so its globals are poisoned in the shadow its guards read. A module destructor unregisters them and clears their shadow, so dlclose leaves nothing behind.