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.