Skip to content
UNDER THE HOOD

The runtime

runtime/ holds the only code WeaveC links into user programs. It is C, built for the host into two archives installed under lib/weavec next to weavec.h. weavec_rt.h is its internal interface (the shadow encoding, the slot words, the descriptors, the entry points), shared by its sources and its test and not installed; compiled code reaches the runtime only through the entry points the guard passes declare, the shadow descriptor and the standard allocation functions.

Archive Source Holds
libweavec_rt.a weavec_alloc.c the size-class arena allocator, its slot words and shadow writes, the quarantine and huge blocks, when to sweep, the allocator’s side of the sweep; the shadow’s reservation
weavec_sweep.c the sweep: stopping the threads, the roots (the image table on Darwin), the conservative mark, recycling
weavec_guard.c the guards’ slow paths (__weavec_rt_guard, __weavec_rt_range, __weavec_rt_range_ok, __weavec_rt_strlen, __weavec_rt_overlap, __weavec_rt_null, __weavec_rt_limit_fail), the frames’ (__weavec_rt_unpoison_stack, __weavec_rt_alloca_poison, _unpoison), the globals’ registration, and the weak __ubsan_handle_out_of_bounds[_abort]
weavec_libc.c the checked library wrappers __weavec_rt_<name>, with their limits for fortified forms, the number parsers, mmap and munmap
weavec_asan.c ASan’s and LeakSanitizer’s interface over WeaveC’s shadow
weavec_owner.c the table of entry points __weavec_rt_dispatch (version 4), and on Darwin the choice of the copy that owns the process
weavec_report.c reports, __weavec_rt_fatal, __weavec_rt_trapping, the WEAVEC_RT_* settings and the counters
libweavec_alloc.a weavec_malloc.c the standard allocation functions over the arena, for the image the archive is linked into

The allocator reserves one arena inside the shadow’s window at first use: one region of 2 GiB (1 GiB when that cannot be reserved) per size class, followed by the metadata. The classes are 16 to 128 bytes in steps of 16, then four per doubling up to 2^30 bytes. A request takes a slot of the smallest class strictly larger than it, so at least one byte after every object belongs to no object. Each slot has one 32-bit word of metadata, value << 2 | state: a live slot’s value is its object’s size, a free or released one’s the next slot on its list. Free lists and the quarantine queues live in the metadata, never in freed memory. An allocation writes 0 over the object’s whole granules, k over a partial last granule and 0xFA over the rest of its slot (up to 64 KiB); a release writes 0xFD over the slot. The shadow of a large object is mapped afresh rather than written, so untouched large blocks cost no memory. Every block is zero-filled.

A released slot joins its class’s quarantine and is never reused from there: only a sweep moves it to the free list (RFC 0036 §3.2), in address order, so that slots are reused in order. A request no class can hold is mapped on its own as a huge block, kept in a table, with at least one poisoned granule after it; at release its mapping is replaced by an inaccessible reservation, unmapped only by a sweep that finds no pointer to it. The allocator validates every release: a release of an interior pointer, of a released or never-allocated slot, or of a stack or global object stops the program through __weavec_rt_fatal; a release of a quarantined block is a double free. A pointer it does not own goes to the next allocator (the pointer’s own malloc zone on Darwin, __libc_free and __libc_realloc or the next free and realloc in lookup order elsewhere).

libweavec_alloc.a defines malloc, calloc, realloc and free strongly; the rest of the family (reallocarray, aligned_alloc, posix_memalign, valloc, free_sized, free_aligned_sized, and malloc_size, malloc_good_size, reallocf on Darwin or memalign, pvalloc, malloc_usable_size elsewhere) is weak, so a program’s own shim over malloc replaces it and keeps the arena. On ELF the executable’s definitions serve every shared library.

Let gained be the bytes the quarantine gained since the last sweep (what the last sweep retained is not counted; released huge blocks count at their size, so a program that frees only huge blocks still sweeps and gives their address space back, RFC 0036 amendment 10), Q the configured quarantine (WEAVEC_RT_QUARANTINE, 16 MiB by default) and live the bytes of live heap blocks. A release (or an allocation) sweeps before it returns (RFC 0036 §3.3) when gained reaches the hard budget max(2 Q, live / 2); when it reaches the soft budget max(Q, live / 4) and the last sweep ended at least eight of its durations ago, so that sweeps take at most about a ninth of the run time and the quarantine grows instead; or when a class’s region is exhausted. The clock is read once per sixteenth of the soft budget released.

The sweep (RFC 0036 §3.4, weavec_sweep.c):

  1. Stop. Take the image table’s lock, every allocator lock and the sweep lock, then stop every other thread: on Darwin with task_threads and thread_suspend, reading each thread’s registers with thread_get_state; on Linux by signalling each task of /proc/self/task (SIGPWR, SIGXCPU when the program handles SIGPWR, or WEAVEC_RT_SWEEP_SIGNAL), whose handler records its stack pointer and waits. A thread that does not stop within a second ends the sweep without recycling: safety never depends on a sweep running.

  2. Mark. Scan conservatively, word by aligned word, for addresses of quarantined slots and released huge blocks (interior addresses count):

    • each thread’s registers, and its stack from its stack pointer;
    • every other readable, writable mapping of the process but the runtime’s own. On Linux, of the files under /dev only shared memory is scanned (/dev/shm, shared anonymous mappings, which show as /dev/zero, and hugetlbfs), not device memory; a sweep that cannot read /proc/self/maps recycles nothing. On Darwin a mapping is asked first for its count of dirty and swapped-out pages (one call), and skipped when it has none, or when it is the kernel’s reclaim ring; mincore then finds the pages to scan. The dyld shared cache is not walked: its writable mappings hold the data of every image in the cache. Instead the runtime keeps, from dyld’s add-image and remove-image callbacks, a table of the writable segments of the images the process loaded from the cache, and scans those;
    • every live heap block and live huge block.

    Released blocks are not scanned. Marks go to a bitmap per class.

  3. Recycle each quarantined slot without a mark to its class’s free list, and unmap each released huge block without one; marked blocks stay.

  4. Resume the threads and release the locks.

The sweep runs on the thread that asked for it and calls nothing that takes a lock a stopped thread could hold: no allocation, no stdio, no dyld. With little live heap a sweep takes about a millisecond on Darwin; it grows with the live heap and the dirtied writable data.

weavec_asan.c (RFC 0036 §5) defines, with ASan’s signatures and meaning, __asan_poison_memory_region (whole granules inside the region become 0xF7, use-after-poison), __asan_unpoison_memory_region, __asan_address_is_poisoned, __asan_region_is_poisoned, __asan_describe_address, __asan_handle_no_return (which clears the stack’s shadow, as before a noreturn call), __asan_get_current_fake_stack, __asan_addr_is_in_fake_stack, __sanitizer_print_stack_trace, __sanitizer_set_report_path, __sanitizer_annotate_contiguous_container, the fiber-switch announcements, and LeakSanitizer’s entry points. What ASan does that WeaveC does not (leak checking, fake stacks) does nothing.

Every image linked by weavec-cc carries the archives. On Darwin the copy whose __weavec_rt_dispatch dlsym(RTLD_DEFAULT, …) finds is the owner; every other copy takes the owner’s arena and shadow descriptors and forwards to it the allocation and the global registration entry points, so one arena, one shadow and one quarantine serve every image (RFC 0033 §6.2, RFC 0036 §4). A table of another layout (another WeaveC version’s runtime) is not used. The arena is also registered as a malloc zone and promoted to the process’s default zone, so malloc from any image, the C library’s own (strdup, getline, asprintf) included, is served by the arena; blocks the system’s zones allocated before that go back to them.

A failed guard prints one line on standard error and traps (SIGTRAP, or SIGILL on x86-64):

weavec: <kind> at <file>:<line>:<column>: <read|write> of <n> bytes at 0x<addr>

with <unknown> for a missing location. For the heap a second line places the address: … is <k> bytes after the <m>-byte heap object at …, or … is inside a released heap block at …; for a fortified call, … is <k> bytes after the <m>-byte destination at …. The kinds are heap-buffer-overflow, heap-use-after-free, stack-buffer-overflow, dynamic-stack-buffer-overflow, stack-use-after-scope, global-buffer-overflow, intra-object-overflow, use-after-poison, null-dereference, unterminated-string, overlapping-copy, and invalid-access for poison the runtime cannot attribute. An index check prints weavec: index-out-of-bounds at <file>:<line>:<column>: index <i>, and a monitor that fails in verify mode weavec: weavec.proven: <kind> at …. The allocator stops an invalid release with weavec: invalid release of 0x<p>: <why>, without a location.

Reports are formatted by the runtime itself, without snprintf (which a program may define) and without allocating. Before every trap, __weavec_rt_trapping unblocks SIGTRAP and SIGILL in the thread and resets an ignored disposition, so the trap ends the program even when it blocked them (a handler the program installed still runs). In report mode (-fweavec-checks=report) each site reports once and the program goes on, with no guarantee. The settings:

Variable Effect
WEAVEC_RT_ABORT=1 report mode traps
WEAVEC_RT_REPORT_LOG=<path> append reports to that file instead of standard error (ignored in set-uid programs)
WEAVEC_RT_BACKTRACE=1 print the stack of the access that stops the program
WEAVEC_RT_STATS=1 print the counters at exit as weavec: runtime: <n> <what> (allocations, releases, recycled slots, huge blocks, slow guards, range guards, string guards, stack unpoisons, sweeps), then the sweeps’ total and longest pause, what the last one retained, and the peaks of the live slots and of the quarantine; on Darwin only the owner prints, counting every image’s work
WEAVEC_RT_QUARANTINE=<bytes> the quarantine’s least growth before a sweep (Q)
WEAVEC_RT_SWEEP_SIGNAL=<n> the signal number that stops threads for a sweep on Linux (default SIGPWR, or SIGXCPU when the program handles SIGPWR)

Reports have no ids: the kind and the line format are the interface. The case runner and the corpus gate attribute traps through this output.