Skip to content

feat!: guard every access by default - #46

Merged
owenthcarey merged 3 commits into
mainfrom
rfc0035-guard-by-default
Oct 7, 2026
Merged

owenthcarey merged 3 commits into
mainfrom
rfc0035-guard-by-default

Conversation

@owenthcarey

Copy link
Copy Markdown
Contributor

Summary

Implements RFC 0035, Guard by default. The order is reversed: weavec-cc now guards every memory access, and removes a guard only where a local rule proves it redundant. The ownership analysis no longer decides what is checked. It is advisory, runs in weavec and under -fweavec-diagnose, and never changes generated code.

After RFC 0034, only 2 of 10 new projects built and passed, three classes of wrong proof had been found, and the guards were doing the catching anyway. Now:

  • New projects (G1). All ten fresh35 projects build with their own builds and CC=weavec-cc, and pass their test suites in trap, report and verify mode: git, tcpdump and libpcap, libtiff, BearSSL, YARA, gawk, libarchive, Tcl, json-c and MicroPython. json-c needs its VALGRIND option and MicroPython a one-line #if change so Clang sees its no_sanitize_address. ASan needs both too.
  • Older sets (G3). All 28 projects in RFC 0033's and 0034's sets build and pass in all three modes. The one false trap found, giflib's sscanf, is fixed by the scanf wrappers.
  • Sealed set (G2), recorded as measured. wolfSSL, xxHash and chibi-scheme pass. sed and m4 did not build because of two runtime bugs, both since fixed: reallocarray on Darwin and a strlen that read past a page. Every workload in the set runs below ASan's ratio.
  • Detection (G4, G5). 60 of 64 blind bug programs stop at the faulty access (ASan: 58). The in-tree sets stop 58 of 61 (ASan: 52) and 44 of 48 (ASan: 42). 13 of 15 historic bugs stop with a report, and one more faults, as under ASan. No fixed twin stops.
  • No wrong proofs (G6). No weavec.proven report in verify mode on any case, suite or benchmark.
  • Start-up (G8). Programs start about 1.2 ms later than without WeaveC (ASan: 90 ms).

Design notes

  • Four LLVM passes (§1, §6, amendments 1, 2 and 6).
    • GuardInsert (PipelineStartEP) puts a marker before each access, before the optimizer can exploit the bug's undefined behaviour.
    • GuardPrune (PeepholeEP) removes in-bounds accesses to locals and globals after inlining.
    • GuardLoop (VectorizerStartEP) versions affine loops with a trip count of at least 16 behind one range check. It also fully unrolls small guarded loops with a constant trip count.
    • GuardExpand (OptimizerLastEP) applies rules 6.1–6.3 (in bounds, dominated, merged within 64 bytes), lays out frames and globals with redzones, and expands each remaining marker into an inline shadow check.
  • Shadow (§2, amendments 3–4). ASan's encoding: one byte per 16-byte granule, with the null page poisoned.
    • A guard is 3 instructions on the fast path: a 16-bit shadow load up to 16 bytes, or a masked 64-bit load for constant sizes up to 113 bytes.
    • Slow paths use preserve_all.
  • Library calls (§2.5, amendment 7). Guards come from the library table's rows. wrapper(NAME) rows go through checked runtime wrappers, which check what the call actually read or wrote:
    • strcpy, strcat and the printf family (including %s, %n and positional arguments);
    • memchr, strchr and the str[n][case]cmp family;
    • the scanf family;
    • gets, mmap and munmap.
  • Unsafe regions (§2.6, amendment 8). no_sanitize("address") functions count as unsafe regions.
  • Zero-initialisation (amendment 11). Locals are zero-initialised up to 4 KiB, and the heap is zero-filled.
  • The enforcement ledger (§9). weavec-ledger version 3 records each access as guarded, proven or unguarded, with a reason.
  • The analysis (§8). The engine, kinds, summaries and weavec --whole-program stay. In weavec, a confirmed definite finding is an error; under -fweavec-diagnose every finding is a warning.
  • The RFC's Implementation amendments (1–13) record every departure from the text, and Measured gates records the results.

Not met, carried forward: run time (G7). These are ratios of instructions retired over the reference compiler's:

Workload WeaveC Gate ASan
Lua 2.24× 2.0× 2.49×
cJSON 1.76× 1.4× 3.05×
brotli 2.59× ≤ ASan 2.32×
zlib 1.28× 1.4× 1.41×

zlib meets its gate. Lua and cJSON are below ASan but above their gates, and brotli is above ASan. Geometric means: 1.5× (fresh35 set A) and 1.76× (set B). fresh34 is 2.04× on instructions and 1.49× on the gate's CPU measure. cJSON's remaining cost is a guard per byte in loops that exit on data, and Lua's is three guards per bytecode instruction.

Not met, carried forward: build time (G9). gawk (1.51×), chibi (1.52×) and several older-set builds measured under load are over 1.5×. Nine units are over 3×: tif_pixarlog.c, lpc.c, compress_fragment.c, yyjson.c, pcre2_match.c, scanners.c, jchuff.c, grammar.c and wolfSSL's api.c. Instruction selection and register allocation over the guards' slow paths dominate. Smaller slow paths are the next step.

Size (H1). The library is 55,992 lines, against RFC 0034's limit of 72,000. This PR adds 19.3K lines and deletes 48.1K.

Breaking changes:

  • These are deleted, with no aliases:
    • Sema check insertion, deferred CodeGen and the prelude;
    • the check planner and the require levels;
    • unit records (.weavec), the link step and the analysis ledger (weavec --ledger);
    • libweavec_chk.a.
  • These flags are deleted:
    • -fno-weavec-runtime;
    • -f[no-]weavec-stack-objects and -f[no-]weavec-global-objects;
    • -fweavec-require=, -fweavec-link= and -fweavec-link-budget=;
    • -fweavec-ledger-format= and -fweavec-print-prelude;
    • -Wweavec-possible.
  • The ownership analysis no longer fails a weavec-cc build. Use -fweavec-diagnose, with -Werror=weavec to make its findings errors.
  • The enforcement ledger is weavec-ledger version 3, and its outcomes are guarded, proven and unguarded.
  • The runtime must be built by Clang, since its slow paths use preserve_all.
  • Locals larger than 4 KiB are no longer zero-initialised.
  • An over-read that lands inside the next object passes without a report, as under ASan.
  • Objects compiled by weavec-cc must be linked by weavec-cc.

Test plan

  • Unit tests: Core 139, Analysis 377, Frontend 66.
  • ctest -R '^runtime$': 21 tests.
  • lit build/dev/test: 130 tests.
  • scripts/run-cases.py: 705 cases pass in trap, verify and --asan (detection 112 of 117; ASan 103).
  • scripts/check-hygiene.py, scripts/check-format.sh.
  • Documentation examples; npm test and npm run build in docs/.
  • scripts/corpus-gate.py --full on the RFC 0033 and 0034 sets (G3), plus fresh35 and sealed35 by hand (G1, G2).

Implements RFC 0035 (docs/rfcs/0035-guard-by-default.md). weavec-cc now
guards every load, store, atomic, memory intrinsic and library-call memory
argument through ASan-style shadow memory, with redzones around stack
frames, globals and heap blocks. A guard is removed only where a local rule
proves it redundant, monitored by -fweavec-checks=verify. The ownership
analysis is advisory: it runs in weavec and under -fweavec-diagnose and
never changes generated code.

BREAKING CHANGE: inserted checks, require levels, unit records, the link
step and the analysis ledger are deleted, with their flags; the enforcement
ledger is version 3; the runtime must be built by Clang.
The runtime reserves its shadow in a constructor on every target: on
Linux nothing did before main, which kept the empty descriptor and passed
every guard. A release of a stack or global object is reported before it
reaches glibc. Reports name the call site of an artificial function, such
as glibc's fortified wrappers (RFC 0035 amendment 14).

Also: clang-tidy findings in the new passes, glibc's missing gets
declaration and ELF's dso_local in the lit tests, and an ASan oracle that
depended on the machine.
The record predated the last wip changes to the passes (late redirect of
read-only calls, inline library guards only for exact constants, loop
unrolling), so CI's quick gate failed on cJSON's proven share. Recorded
from a full --quick run; adds the configs that had no record.
@owenthcarey
owenthcarey merged commit 0f0d226 into main Oct 7, 2026
12 checks passed
@owenthcarey
owenthcarey deleted the rfc0035-guard-by-default branch October 7, 2026 23:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant