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
Section titled “The allocator”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.
When to sweep
Section titled “When to sweep”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
Section titled “The sweep”The sweep (RFC 0036 §3.4, weavec_sweep.c):
-
Stop. Take the image table’s lock, every allocator lock and the sweep lock, then stop every other thread: on Darwin with
task_threadsandthread_suspend, reading each thread’s registers withthread_get_state; on Linux by signalling each task of/proc/self/task(SIGPWR,SIGXCPUwhen the program handlesSIGPWR, orWEAVEC_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. -
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
/devonly 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/mapsrecycles 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;mincorethen 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.
-
Recycle each quarantined slot without a mark to its class’s free list, and unmap each released huge block without one; marked blocks stay.
-
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.
ASan’s interface
Section titled “ASan’s interface”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.
One runtime per process on Darwin
Section titled “One runtime per process on Darwin”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.
Reports
Section titled “Reports”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.