RFC 0030: Prove or trap — one safety semantics with compiler-enforced checks
- Status: Implemented (as amended by RFC 0031, which replaced its engine, took over its gates G10, G14 and G15, and amended G10’s limit to 65; and by RFC 0032, which adds the outcome
guarded, a runtime in every enforcing build, guards for temporal facets, the require levelguardedand ledger version 2. Where this text says “RFC 0032” for records in object sections, baselines and archives, it means the adoption RFC, which is now planned as RFC 0033. Its analysis (§2–§9) is superseded by RFC 0036, which deletes it) - Authors: WeaveC authors
- Created: 2026-09-18
- Tracking issue: TBD
- Supersedes / superseded by: Supersedes RFCs 0018–0029 in full.
Replaces the guarantee statement in RFC 0001, Soundness, and
docs/pages/reference/guarantees.md. Amends RFCs 0002–0017 wherever they set a default severity, defineannotation-requiredoranalysis-incomplete, give unknown callees borrow-only effects, letWEAVEC_UNSAFEsuppress diagnostics, trustWEAVEC_ASSUME, or state that WeaveC adds no runtime instrumentation (§19 lists each amendment). Its enforcement is superseded by RFC 0035 and its analysis (§2–§9) by RFC 0036.
RFC 0035 superseded this RFC’s enforcement and kept its analysis as advisory; RFC 0036 deletes that analysis (§2–§9: the
weavectool, its diagnostics and every annotation butWEAVEC_UNSAFE). The RFC keeps its status as the record of what was implemented.
This RFC was drafted before implementation. On 2026-09-18 the project owner
accepted every recommendation of the milestone analysis. The owner
explicitly authorized drafting this RFC and then implementing it end to end
in one large commit. The authorization covers four decisions: runtime checks
are on by default in weavec-cc, locals and heap allocations are
zero-initialised in enforcing modes, checked mode is deleted outright, and
the mechanical deletion of generated evidence lands in the same commit.
Accepted status records that authorization; it is not a separate review. The
status becomes Implemented only when every gate in Acceptance gates passes
on the final tree.
Where this RFC departs from a recorded resolution of the owner’s decisions, because review showed the resolution unsound or infeasible as written, the text says so at that point with the word Departure and the reason.
Summary
Section titled “Summary”WeaveC records exactly one outcome for every safety facet (spatial, null,
temporal) of every memory operation it compiles: proven by the analysis,
checked by a compiler-inserted runtime check, a definite violation (a
build error), or unresolved or trusted with a reason from a closed list.
weavec-cc turns every unproven spatial or null obligation it can express
into a trapping check, inserted into the AST through Sema before a deferred
CodeGen, with no ABI change and no runtime library in the default mode.
Temporal safety stays static: possible temporal bugs remain visible as
warnings and ledger rows, and only definite violations break builds. Each
translation unit gets a guarantee statement under five named assumptions
and a complete JSON/SARIF ledger; unknown code gets sound defaults,
WEAVEC_UNSAFE means trusted raw operations rather than silence,
WEAVEC_ASSUME becomes a checked assertion, one declarative LibrarySpec
table replaces three library models, and ecosystem attributes discharge
obligations. Checked mode (RFCs 0018–0029) is deleted with its idiom
families, formats, reports, cache, 243 CTest populations and about 283K
lines of generated evidence. The existing FunctionDataflow engine stays as
the prover behind an engine-agnostic seam that RFC 0031 can replace, and
library code (lib, include, tools) shrinks from 77.2K lines to at most
62K.
Motivation
Section titled “Motivation”WeaveC v0.10.0 has two modes. Neither delivers the README’s promise, and
the costs are measured. The numbers below come from a Release build of
e0e2bd6 on 2026-09-18; S0 imports the probe programs, root-cause repros
and corpus configuration into test/cases and test/corpus, so each
number can be reproduced.
The default mode is noisy. On the 11-configuration corpus (sds, cJSON, jsmn, log.c, printf, linenoise, cJSON-program, zlib, lua, linenoise-program, jansson) it reports:
- 301 bug-claiming reports, among them 56 double-free, 32 use-after-free, 186 null-dereference and 22 leak;
- 40
annotation-required; - 4,239
analysis-incomplete, 3,990 of them on Lua.
Hand triage of 62 of the 301 bug-claiming reports found:
- 52 false positives;
- 7 true reports that are only out-of-memory paths;
- 2 benign leaks at exit from
main; - 1 boundary case.
Extrapolated, about 92% of the bug claims are false, and none is a bug upstream would fix.
The causes are structural. Allocator and realloc wrappers lose the
correlation between their result and what they freed (about 46%; jansson’s
jsonp_realloc alone yields 65 reports). Object and state-machine
invariants cause about 18%, integer–nullness correlation about 12%, array
cells in cleanup loops about 10%, and guard functions such as
cJSON_IsString that do not refine their callers about 6%. Because
may-errors are errors, zlib’s own make fails with 11 null-dereference
errors that look false.
The default mode is also silently unsound. 113 soundness probes were checked: 85 ASan-confirmed bug programs and 28 correct twins. The default mode catches 36 of the 85 bugs. 42 produce no output at all: exit 0, not even an incompleteness warning. The silent bugs include:
- use-after-free through globals, through fields across helpers and through
qsort, thread and signal callbacks; strtokandgetenvstatic state;- pointers copied byte by byte;
- unknown system APIs;
- 16 spatial bugs:
strcpyofargvinto a stack buffer, negative indices, malloc size overflow, begin/end walks that overrun by one, flexible-array overflow, and others.
The engine already computes that 86% (linenoise), 99% (sds) and 80% (cJSON) of spatial checks are unresolved, and never says so. Several unsound choices are deliberate:
- unknown callees are assumed to borrow their arguments with no effects;
- system-header callees are exempt from
annotation-required; - library callback arguments are ignored;
WEAVEC_UNSAFEsuppresses even a definite use-after-free (probe 45);- a contradicted
WEAVEC_ASSUMEis trusted (probe c03); - a lying cross-TU annotation is never verified (probe 38).
The code weavec-cc emits is byte-identical to clang’s: at runtime nothing
is enforced. RFC 0001 claims completeness, while guarantees.md says a
quiet run certifies nothing.
Checked mode plateaued and is not affordable. Complete contracts across RFCs 0020–0028 stopped growing:
| After RFC | 0020 | 0021 | 0022 | 0023 | 0024 | 0025 | 0026 | 0027 | 0028 |
|---|---|---|---|---|---|---|---|---|---|
| Complete contracts (of 1,619 corpus definitions) | 99 | 120 | 124 | 130 | 142 | 148 | 148 | 148 | 150 |
RFCs 0026–0029 added about 157K lines, including data, for +2 contracts. That is the evidence for not re-deriving any idiom recognizer here. Other measured costs:
- cJSON.c takes 45–65 s in checked mode (0.8–1.3 s ordinary) and produces no object.
- Lua takes more than 1,188 s at 4.7 GB, which is RFC 0029’s open blocker.
- Checked mode rejects 17 of the 28 correct probe twins.
- It accepts two definite double frees through aliased globals and fields (c10, c10c).
- Enabling
--checked-reportsuppresses correct ordinary diagnostics. - Acceptance is unpredictable:
drop(l)is complete, butdrop_clear(l), the same loop plusl->head = NULL, is incomplete.
Adoption breaks at the entry points.
weavec --whole-program -p build, the README’s headline command, segfaults (tools/weavec/main.cpp:224-241).-fdiagnostics-format=sarifcrashes both tools.- Sidecars are not carried into static archives, so Lua’s real
makelinksliblua.aand its link step silently sees one unit. - Checked-mode populations take 82% of Linux CTest time (275 of 334 s) and 79% of the ASan job’s CTest time (907 of 1,143 s); the ASan job as a whole runs 41 minutes of its 45-minute limit.
- About 283K lines (18 MB) of generated per-RFC results are committed.
What works elsewhere. Every system that has delivered memory safety for
legacy C proves what it can and checks the rest at runtime: CCured, Checked
C, Clang’s -fbounds-safety (checks pruned by ConstraintElimination), and
Rust’s own bounds checks. Purely static tools either change the language,
restrict the domain, or disclaim a guarantee. For WeaveC, imprecision should
cost a branch, not a false error or a silent miss. Measured on Lua, a
trapping null check on every dereference costs 1.02x.
The riskiest mechanism has already been exercised. A prototype deferred
Clang CodeGen until after an analysis pass and inserted checks through Sema.
It produced byte-identical objects for 111 of 111 corpus TUs at -O2, at
-O0 -g and with -flto=thin. Its traps were correct in load, store,
compound-assignment, ++, & and -> contexts.
The cost of not doing this is another milestone of idiom recognizers on a mode nobody can run on Lua, while the default mode stays both noisy and silent.
Soundness
Section titled “Soundness”This section replaces the guarantee statement of RFC 0001, Soundness, and
the text of docs/pages/reference/guarantees.md.
The guarantee
Section titled “The guarantee”Let U be a translation unit compiled by weavec-cc in an enforcing
mode: -fweavec-checks=trap (the default) or verify, or report with
WEAVEC_RT_ABORT=1 in the environment, with zero-initialisation on (the
default). The guarantee covers every execution of a program containing U’s
object code, and every operation site s emitted from U outside a
WEAVEC_UNSAFE region:
- (S) If the spatial facet of s is proven or checked, then the bytes s accesses lie inside the object its pointer operand was derived from (§7.4 defines the object). For a checked facet, the program instead traps at s before the access.
- (N) If the null facet of s is proven or checked, then s does not dereference a null pointer. For a checked facet, the program instead traps first.
- (T) If the temporal facet of s is proven, then the object s accesses has not been released and its lifetime has not ended. For a release site, the object is released at most once and is the start of a live allocation of the releasing family.
- (V) No violation outcome reaches emitted code unguarded. A violation
fails the compile; if its error is lowered to a warning
(
-Wno-error=weavec-<id>), the site is emitted behind a check that traps (§3.4).
-fweavec-checks=report without WEAVEC_RT_ABORT=1 is a rollout aid, not
an enforcing mode: a failed check is reported and the access proceeds, so
(S) and (N) do not hold for its checked facets. Its ledger is the same as in
trap mode, because the ledger always describes the enforcing build.
These hold under assumptions A1–A5:
- A1 — callers. Callers outside U of U’s exported and address-taken functions pass arguments that satisfy what the callee relies on. Each pointer argument is null or satisfies the kind the callee relies on: its declared kind, or by default Single, at least one live object of its pointee type (§7.3). Declared requirements hold. No two arguments that U treats as owning refer to the same object.
- A2 — trusted callees. Every callee whose effect a facet records as
trusted behaves as its contract says:
system-api,library-spec,extern-contractorexternal-unit(§2.4). Values stored from outside into a function-pointer slot with known targets behave like those targets. - A3 — other code maintains the heap invariants. Code outside U (other units, libraries, the runtime) leaves every pointer U can reach through parameters, results and globals in one of two states. It is either null, or it points to a live object with at least one element of its type. Pointers stored in owning slots are unique. Exact counted-field invariants on header structs that U relies on hold (§7.6).
- A4 — no concurrency outside trust. No other thread, signal handler or
longjmpchanges memory that U accesses during U’s operations, except at sites marked for it:trusted(concurrency)orunresolved(setjmp)facets, and the checked facets §5.3–5.4 substitute for flow proofs. - A5 — initialisation. The linked allocator answers the usable-size
query (
malloc_usable_size,malloc_size) consistently with itsmalloc. Pointer-typed memory that U reads and that did not come from a zero-initialising source (automatic storage whose declaration no jump bypasses, static storage, and the lowered allocation family of §11) was written before U loads it. This covers memory from non-lowered allocators, unknown callees and U’s own free lists. The ledger counts the non-lowered allocation calls U makes (§12.1), and the link step warns when a unit defines the allocator (§13.2).
The link step (§13) verifies A1 and A3 where the callers and stores are visible in units with WeaveC records, and prints one line listing what remains.
Blame property. Suppose an execution of U’s code performs a memory-safety violation at site s (an out-of-object access, a null dereference, or a use or release of a released object). Then at least one of these holds:
- the program trapped at a checked site first;
- the violated facet of s is unresolved or trusted;
- the fact that proved s was established at a creation point, a store, a call, a boundary or a function entry whose own facet is unresolved or trusted, or that rests on A1–A5 at an interface of U;
- WeaveC has a bug.
The ledger names the site in cases 2 and 3; in case 3 it is not always the
nearest cause. The ledger records outcomes, not proof dependencies. Case 3
is therefore stated per facet. A spatial or null proof depends only on the
pointer’s kind at its creation and on flow facts local to the function
(§7.4); where a kind rests on a default, the call or store that supplied
the value carries a row (§7.3). A temporal proof depends on the function’s
entry assumptions and on the boundaries it crossed; a boundary row that
breaks an entry assumption is propagated to every use that relied on it
(§9.4). -fweavec-checks=verify is the monitor for case 4 (§10.7).
Bugs caught
Section titled “Bugs caught”Classes caught at compile time (definite violations are errors):
- use of a released or moved object, double release, release of non-heap or interior storage, and a release through the wrong family, when the release holds on every path (§3.1);
- dereference of a pointer that is null on every path from a non-allocator source;
- an access past an object of exact extent that is out of bounds for every
value the facts allow, including library calls whose required length is
known (for example
strcpyinto a smaller constant buffer, and a VLAvla[n]); - overlapping
memcpy/strcpy-family copies with known ranges (reported asout-of-bounds); - writes into string literals (writable extent zero) and format strings whose conversions read more arguments than are passed;
- a contradicted
WEAVEC_ASSUME; - a declaration whose annotation contradicts its definition, including across translation units at the link step (probe 38).
Classes caught at runtime by a trap at the offending access (the checked outcome):
- every dereference whose null outcome is not proven (unknown, maybe-null or allocation-failure values, and uninitialised pointers made null by zero-initialisation);
- every index, cursor dereference and library-call length whose extent is exact (an array or VLA object, an allocation size in scope, a surviving field invariant) or declared, and has a nameable term (§7.1, §10.3).
Temporal bugs on some paths only are reported as warnings with the same id
and “may” wording. Examples: a free inside a loop that a later iteration
repeats (zlib minigzip’s fclose(stdout)), and a use after a qsort
comparator freed the object.
Illustrative rejections (each has a case in test/cases):
char *p = malloc(4); if (!p) return; p[4] = 0; // error: out-of-boundsfree(p); p[0] = 1; // error: use-after-freeif (n == NULL) return n->v; // error: null-dereferencechar b[4]; int i = 10; WEAVEC_ASSUME(i < 4); b[i]; // error: contradicted-assumptionmemcpy(buf + 2, buf, 8); // error: out-of-bounds (overlapping ranges)WEAVEC_UNSAFE { free(p); p[1] = 2; } // error: use-after-free (no longer hidden)and runtime traps:
int a[4]; int i = atoi(argv[1]); if (i < 4) return a[i]; // traps for i < 0char buf[8]; strcpy(buf, argv[1]); // traps when argc < 2 or strlen(argv[1]) >= 8int first(const char *s) { return s[0]; } ... first(c->name) // traps on NULL, also when first is exportedBugs deliberately not caught
Section titled “Bugs deliberately not caught”- Temporal bugs at runtime. There is no quarantine allocator and no memory tagging. A temporal facet that is not proven is a warning (when possible) or an unresolved row. Enforcing it is RFC 0031’s temporal backstop.
- Accesses whose extent is not exact or declared, or has no nameable
term. Examples are the sds header idiom
s[-1],container_of, cursors stored in unannotated fields, buffers from unknown callees indexed beyond element 0, and aqsortcomparator’sconst struct T *a = pa. These areunresolved(unknown-extent)orunresolved(unknown-index), never checked against a lower bound (§7.1). Closing them needs a heap-bounds runtime (RFC 0031) or annotations. - Pointers created by reinterpretation. An integer-to-pointer
conversion makes an RFC 0004 raw pointer: using it outside an unsafe
region is an
unsafe-operationerror, as today, and inside one it istrusted(unsafe). Union punning, byte-wise or partial copies of pointer objects, andva_argof pointer type are not raw under RFC 0004; uses of such pointers areunresolved(raw-cast). The null check still runs, but a forged non-null pointer passes it. - Uninitialised scalars. They are outside the three facets. Enforcing
modes define them as zero, which neutralises the class (probes 08, 32);
with
-fno-weavec-zero-initthey are not covered. - Data races and asynchronous signals. These are assumption A4. For
places shared with thread or signal entry points (not
atexithandlers, §5.3), temporal facets aretrusted(concurrency), and null and spatial facets that only flow facts would prove are checked instead. setjmp/longjmp. Every temporal facet of a function that callssetjmpisunresolved(setjmp). Alongjmpinto a dead frame is not detected.- Leaks. A leak is a warning and never part of the guarantee.
- Integer overflow as such, type confusion that stays within an object,
and floating point. RFC 0017’s
invalid-integer-operationis unchanged. A size overflow is caught only where it leads to an out-of-bounds access (probe 06 traps). - Inline assembly. An
asmstatement applies the unknown-callee default to its pointer operands (§5.7). Its own memory accesses are outside the model. - Code without WeaveC metadata (archives, shared libraries, objects
compiled by another compiler). Its calls are
trusted(external-unit), and the link step names each such input once (unanalyzed-input).
Accepted false positives and false traps
Section titled “Accepted false positives and false traps”-
Definite errors are designed to be true. A definite error needs a record on every path that no lossy summary produced (§3.1, §9.1), a null value from a non-allocator source, or a bounds verdict against an exact extent that fails for every value. The remaining source of false errors is engine imprecision in values: a fact that is must in the abstraction but not in the program, such as an infeasible path the engine cannot refute. Gate G9 allows at most 10 definite errors on the corpus, each triaged true.
-
Possible temporal warnings can be false. Examples are correlated conditions (
if (c) free(p); … if (!c) use(p);, probe 41e) and state-machine invariants. They are warnings, which-Werrorin project flags does not promote. Gate G10 caps them at 60 on the corpus, every one triaged. -
False traps. A trap in a correct program needs a requirement or invariant tighter than the real one. Six rules prevent this:
- a check compares only against an exact extent or a declared kind, never against a lower bound such as the Single default (§7.1);
- the extent of a pointer made from a member or element is the whole object, not the sub-object, and trailing arrays are flexible (§7.4);
- parameter requirements come only from must-access rules, guarded by loop entry and void when a call before the access may not return (§7.5);
- field invariants must be exact, and are dropped on any write the analysis cannot see (§7.6);
a + i,&a[i]and conversions outside required positions are never checked;- inferred requirements are enforced at call sites only for static functions.
A correct program can still trap when it breaks C’s rules in a way that happens to work. Examples are reading one element past an array that is followed by known memory, and dereferencing a pointer built from an integer inside a struct hack. Such code needs
WEAVEC_UNSAFE. Gate G11 requires 0 traps in every corpus project’s own test suite. -
Zero-initialisation changes behaviour only for programs that read indeterminate values. Such programs are already undefined in C.
Where this is less or more sound than v0.10.0
Section titled “Where this is less or more sound than v0.10.0”More sound:
- unknown callees, callbacks,
setjmpand system APIs no longer default to silent success; WEAVEC_UNSAFEno longer hides definite violations;WEAVEC_ASSUMEis checked;- cross-TU annotations are verified at link;
- a use through a pointer that may alias a released object is no longer proven (§3.1);
- every unproven null or spatial obligation is either checked or listed.
Less sound in two respects:
- Possible temporal findings are warnings rather than errors, so a build
that used to fail on a real path-dependent use-after-free now succeeds.
The ledger and the warning keep the finding visible, and
-fweavec-require=checkedrestores fail-closed behaviour. - With the
weavectool, or with-fweavec-checks=none, possible null and spatial findings that v0.10.0 reported as errors (“may be null”, “may be out of bounds”) produce no diagnostic and no check. They are counted as “checkable (not enforced)” in the summary line and the ledger, and-fweavec-require=proventurns them intounchecked-operationerrors.weavecalone is therefore not a bug finder for these classes; the checks in aweavec-ccbuild are.
This trade is deliberate: 92% of today’s errors are false, and a build that cannot pass is not a guarantee.
Projected outcomes on the soundness probes
Section titled “Projected outcomes on the soundness probes”The projection below classifies the 85 probe bugs; gate G4 measures the actual outcomes.
| Class | Probes | Count |
|---|---|---|
| Reported today, still reported (error or warning) | the 36 caught today, plus 26 and 41 (mislabelled today) | 38 |
| New: definite error | 09 (call-site requirement), 19, 20b, 21, 23, 25, 25b, 38 (link), 45, c03, c06, 18 (format arity), 46 | 13 |
| New: runtime trap | 06, 07, 07b, 20, 24, 29, 31, 36, 39, 49, c09, c12 | 12 |
| New: possible warning | 25c (setenv invalidates getenv), 47 (qsort comparator) |
2 |
| Non-silent ledger row only | 02, 02c, 02d (dangling-escape, propagated to the bug site, §9.4); 10, 17, 42, c04, c14 (raw-cast); 14 (unknown-callee); 14b, 37, c13 (unknown-extent); 14c (caller-contract); c01 (inexpressible); 15, 15b (concurrency); 16, 16b (setjmp) | 18 |
| Neutralised by zero-initialisation | 08, 32 | 2 |
The projected total reported is 65 of 85, with 0 silent. The gate floor is 55, against 36 today.
Detailed design
Section titled “Detailed design”1. Architecture and pipeline
Section titled “1. Architecture and pipeline”Components by layer. New files are listed without “(edited)”:
| Layer | Component | Role |
|---|---|---|
| Core | Ledger.h/.cpp |
Site, Facet, SiteOutcome, UnresolvedReason, TrustReason, counts, rollup, summary line text |
| Core | PointerKind.h/.cpp |
the kind lattice and extent terms (§7.1) |
| Core | LibrarySpec.h/.cpp + LibrarySpec.txt |
the declarative library table and its parser (§8) |
| Core | CheckPlan.h |
check templates and operand terms as pure data (§10.1) |
| Core | FnSlots.h/.cpp |
function-pointer slot constraints and solver (§9.3) |
| Core | Summary, SummaryIO (edited) |
summary format 27: checked languages removed; kinds, reliance flags, lossy bits and outcome cases added (§7.3, §9.1) |
| Core | Moves, Nullness, Borrow (edited) |
certainty bits (§3) |
| Analysis | AttributeReader |
declared kinds from attributes and annotations (§7.2) |
| Analysis | KindInference |
slot kinds, parameter kinds and reliance, must-access requirements, field invariants (§7.3–7.6) |
| Analysis | SiteCollector |
syntactic enumeration of every site before any dataflow (§2.6) |
| Analysis | SlotCollector |
per-TU function-pointer slot constraints (§9.3) |
| Analysis | BoundaryInvariants |
dangling-escape and second-owner decisions and their propagation (§9.4) |
| Analysis | CheckPlanner |
checked requirements → core::CheckPlan, and the expressibility decision (§10) |
| Analysis | LedgerAdapter, SafetyEngine |
the engine seam (§14) |
| Analysis | DataflowEngine |
SafetyEngine implemented over FunctionDataflow |
| Frontend | DeferredCodeGenConsumer |
buffers CodeGen callbacks until the analysis and rewrites finish (§10.5) |
| Frontend | CheckEmitter |
Sema-built AST rewrites from CheckPlan (§10.6) |
| Frontend | Prelude |
the helper definitions injected into predefines (§10.2) |
| Frontend | ZeroInit |
allocation lowering and -ftrivial-auto-var-init=zero (§11) |
| Frontend | LedgerWriter |
JSON and SARIF writers and fingerprints, over llvm::json and llvm::SHA256 (§12) |
| Frontend | UnitRecord |
format-28 framing and payload codec, over llvm::json and llvm::SHA256 (§13) |
| Frontend | ProgramAnalysis, Sidecar, Driver (edited) |
link verification, program ledger, CLI |
| runtime | runtime/weavec_rt.c |
about 100 lines, linked only for -fweavec-checks=report |
| runtime | runtime/weavec_chk.c |
out-of-line copies of the helpers, linked only for PCH and module builds (§10.9) |
The writers and the record codec live in Frontend because Core may not use LLVM, and a Core JSON and SHA-256 implementation would duplicate LLVM’s.
The components in the Analysis rows above LedgerAdapter read ASTs, the
LibrarySpec and the ledger; they never include Dataflow.h. Only
DataflowEngine.cpp, FunctionAnalysis.cpp, CallbackSummaries.cpp,
CallContextSummaries.cpp and the Dataflow*.cpp files may include it.
The weavec-cc compile step for one C translation unit runs as follows:
- Clang parses the unit.
DeferredCodeGenConsumerforwards Sema set-up and records every CodeGen callback without running it. - At
HandleTranslationUnit:AttributeReaderand the syntactic part ofKindInferencecompute declared kinds, slot kinds, parameter kinds and must-access requirements. None of this uses engine facts.SiteCollectorenumerates the sites of every emitted function. It runs after the kinds, because PtrArith and Cast sites exist only in required positions (§7.4).SlotCollectorcollects the unit’s slot constraints and solves them locally.- The engine runs through
LedgerAdapter:DataflowEngine, i.e.TranslationUnitAnalyzeroverFunctionDataflow. BoundaryInvariantsand the field-invariant part ofKindInferenceconsume the facts the engine published.LedgerAdapter::finishfills the default outcomes, propagates boundary rows, runsCheckPlanner::planto decide which checked requirements are expressible (§10.3), and produces the unitLedgerand itsCheckPlan. This happens in every mode and inweavec, so the ledger never depends on whether checks are emitted.
- Definite violations are reported as errors and possible temporal
findings as warnings. Under
-fweavec-require,unresolved-operationandunchecked-operationerrors are added, from the planned ledger. - If an error was reported, the recorded callbacks are replayed unchanged
and CodeGen drops the module, as today. Otherwise, when checks are on,
CheckEmitterapplies theCheckPlan, and then the callbacks are replayed. A rewrite that Sema rejects at this point is a WeaveC bug: it is reported as an internal error that fails the compile (§10.6), never silently dropped. - The object is written. The format-28 record is written to
<object>.weavec, the unit ledger to-fweavec-ledgerif given, and the summary line to stderr under-fweavec-summaryor-fweavec-ledger.
The link step (§13) reads the records and solves function-pointer slots program-wide. It verifies declarations and exported requirements across units, then re-runs the engine for temporal refinement. Finally it writes the program ledger.
weavec (the libTooling tool) runs the same steps 1–3 and 5, without
CodeGen, per source or as one program with --whole-program.
Refinement is split by facet. Spatial, null and assertion outcomes are decided once per TU, because they decide the emitted code, and the link step copies them verbatim. Temporal outcomes are refined at link.
2. Sites, facets and outcomes
Section titled “2. Sites, facets and outcomes”2.1 Site kinds
Section titled “2.1 Site kinds”A site is one operation in one emitted function. Its identity is
{function, ordinal, kind, location}, where the ordinal is the site’s
0-based position in source order within the function.
| Kind | JSON | Created by | Facets |
|---|---|---|---|
| Deref | deref |
*p, p->f, and p[0] with an integer constant zero index, where p points to an object type (dereferences of function pointers are Call sites) |
spatial, null, temporal; only null when the lvalue is used solely as the operand of & or decays to a pointer (&p->f, p->arr) |
| Index | index |
e[i] other than the Deref case; *(p + i) and *(p - i); subscripts of array-typed lvalues |
spatial; plus null and temporal when the base is a pointer value; no site when the lvalue is used solely as the operand of & (&p[i] is PtrArith) |
| PtrArith | ptr-arith |
p + i, p - i, ++p, p++, &p[i] whose result flows directly into a required position (§7.4) |
spatial |
| Cast | cast |
a conversion from T * to U * (explicit or implicit, including from void *) where U is a complete object type wider than T (void and character types count as size 1), whose result flows directly into a required position (§7.4); a store of a pointer into a slot with a declared kind |
spatial |
| IntToPtr | int-to-ptr |
a conversion from an integer to a pointer other than a null pointer constant (an RFC 0004 raw origin); va_arg(ap, T *) |
spatial, temporal |
| LibCall | lib-call |
a direct call whose callee resolves to a LibrarySpec entry (§8, including its aliases, and only when the program does not define the function), or an indirect call whose solved slot contains such a function (§9.3), where the entry has no release effect |
spatial, null and temporal, as the entry’s requirements name them; the temporal facet also carries the boundary invariants when the entry has a callback clause (§9.4) |
| Release | release |
a call whose LibrarySpec entry releases or reallocates an argument (free, realloc, fclose, freeaddrinfo, …) |
spatial (start of allocation), null (when the entry forbids null), temporal (live, of the releasing family) |
| Call | call |
every other call expression, direct or indirect; and every function exit (each return, the end of the body, and each call to a function that does not return: exits or noreturn in the table, or declared noreturn), distinguished by boundary: "call" or "exit" |
temporal; null on the callee operand of an indirect call; spatial and null for each argument with a declared requirement, an enforced inferred requirement (§7.2, §7.5) or a parameter whose callee relies on its default kind (§7.3) |
| Assume | assume |
each WEAVEC_ASSUME(e) |
assertion |
| Raw | raw |
a dereference, subscript or release through a pointer with an RFC 0004 raw origin: declared WEAVEC_RAW, converted from an integer, loaded through a raw pointer, or handed out as raw by a callee |
spatial, null, temporal |
A facet applies only where it has meaning:
- The null facet exists only when the operand is a pointer value, not an array lvalue.
- The temporal facet exists only when the pointed-to object’s lifetime can end before the site. An array lvalue of automatic storage in scope, or of static storage, has none.
&*p and a member designation on a null pointer constant (the old
offsetof idiom, &((T *)0)->f) create no site. Sites in unevaluated
operands (sizeof, _Alignof, typeof, unselected _Generic branches)
do not exist, except in operands of variably modified type, which C
evaluates. Sites in constant expressions (file-scope initialisers, case
labels, array bounds) are decided statically. They can be proven or a
violation, and are otherwise unresolved(inexpressible); they are never
checked. Sites whose operand is a pointer into a non-default address
space, and sites inside the shared operand of a BinaryConditionalOperator
(a ?: b) or another OpaqueValueExpr source, are
unresolved(inexpressible) for every facet a check would serve, because the
helper cannot take the operand or the operand cannot be replaced in place.
SiteCollector walks the semantic form of initialiser lists and
deduplicates sites by Stmt *, so shared subtrees are counted once.
The assertion facet is used only by Assume sites; the three safety facets are spatial, null and temporal. Departure: the resolutions name three facets. An assumption is not a memory access, and folding it into one of the three would make the per-facet counts meaningless, so it gets a fourth facet that no other site kind has.
2.2 Outcomes
Section titled “2.2 Outcomes”core::SiteOutcome (the name core::Outcome is taken by the RFC 0006
result classes):
| Outcome | JSON | Meaning |
|---|---|---|
| Proven | proven |
The facet holds on every execution of an enforcing build, under A1–A5 |
| Checked | checked |
Not proven. A check at the site traps before the operation if the facet fails. The check is emitted when checks are enabled; with -fweavec-checks=none or in weavec, the ledger still says checked and the summary line says “checkable (not enforced)” |
| Violation | violation |
Fails on every execution that reaches the site; always paired with an error diagnostic (a warning only when lowered by -Wno-error, in which case the site traps, §3.4) |
| Unresolved | unresolved |
Neither proven nor checkable; carries an UnresolvedReason |
| Trusted | trusted |
Holds if a named trust assumption holds; carries a TrustReason |
2.3 Unresolved reasons (closed list)
Section titled “2.3 Unresolved reasons (closed list)”| Reason | Meaning |
|---|---|
unknown-extent |
No exact extent or declared kind (§7.1) covers the access for the object the pointer points into. A lower-bound kind (Single, a default, an inferred requirement) covers only the elements it guarantees |
unknown-index |
The object’s extent is known but the pointer’s offset from the object’s start is not (the base is not identifiable) |
inexpressible |
The analysis knows the obligation’s terms, but no check can be written at the site: a term has no C name there (it lives only in a snapshot place), is volatile, needs a function call other than the prelude’s, is wider than 64 bits, names a place the call’s arguments or the wrapped operand may write, or the site is in a constant expression or a shared operand (§2.1, §10.3) |
may-released |
The object may have been released on some path reaching the site; always paired with a possible-warning diagnostic |
may-moved |
The value may have been moved out on some path; paired with a warning |
may-alias-released |
Earlier on some path, an object was released that the site’s pointer may alias: the pointer was loaded from a place the analysis cannot prove distinct from the released one (§3.1); no diagnostic |
may-invalid-release |
A Release site may release a non-heap, literal or interior pointer on some path (spatial facet); paired with an invalid-release warning |
may-mismatched-release |
A Release site may release an object of another family on some path (temporal facet); paired with a mismatched-release warning |
may-dangle |
A return or store at a Call exit boundary may let a value outlive its storage on some path (temporal facet); paired with a lifetime-too-short warning |
may-conflict |
A move or release may conflict with a loan that is live on some path (temporal facet); paired with a conflicting-borrow warning |
unknown-callee |
An earlier call to a function with no body, summary, LibrarySpec entry or declared ownership contract (or an asm statement) may have released, retained or replaced the value (§5.1) |
callback |
As unknown-callee, for an indirect call through a function-pointer slot with no known target (§9.3) |
setjmp |
The function calls setjmp; values may be stale after a longjmp (§5.4) |
budget |
The function exceeded its analysis budget; default outcome (§5.5) |
unanalysed |
The engine did not reach the site or cannot model the construct (the old “unsupported …” and internal-limit incompleteness reasons) |
raw-cast |
The pointer was created by reinterpretation that RFC 0004 does not mark raw: va_arg of pointer type, a union member whose last visible write was a non-pointer member, or a byte-wise or partial copy of a pointer object. Also the outcome of an IntToPtr site’s own facets outside an unsafe region |
dangling-escape |
At this boundary, a place reachable from a parameter or global may hold a released pointer or a pointer to storage whose lifetime has ended; or the site relies on the entry assumption for a place class that such a boundary breaks elsewhere (§9.4) |
second-owner |
At this boundary, two owning places reachable from parameters or globals may hold the same owned object; or, as above, the site relies on an owner-uniqueness assumption a boundary breaks (§9.4) |
no-zero-init |
The pointer may be uninitialised: zero-initialisation is disabled, a jump can bypass the variable’s declaration, or the storage came from an alloca that was not zeroed (§11) |
2.4 Trust reasons (closed list)
Section titled “2.4 Trust reasons (closed list)”| Reason | Meaning |
|---|---|
unsafe |
The site is inside a WEAVEC_UNSAFE region (§6.1): its spatial and null facets, and every facet of a Raw or IntToPtr site (the only temporal use of this reason) |
system-api |
The facet depends on a function declared in a C library, POSIX or platform header (§5.2), and not in the LibrarySpec, having borrow-only effects, or on such a header’s attributes |
library-spec |
The facet depends on a LibrarySpec statement about the callee’s own behaviour: a static(S) or interior-state(S) result, a retain/reads/invalidates relation, or a callback clause (§8.2) |
extern-contract |
The facet depends on the declared ownership contract of a function defined outside the TU (§5.1), or on external values in a function-pointer slot behaving like its known targets (§9.3) |
caller-contract |
The facet depends on an inferred extent requirement of an exported or address-taken function that callers outside the TU must meet (A1, §7.5). Never used for nullability |
external-unit |
The facet depends on code in a link input that has no WeaveC record |
concurrency |
The temporal facet concerns an object shared with a thread entry point or signal handler, or a null or spatial facet of such an object has no expressible check (§5.3) |
Adding a reason to either list requires an RFC.
2.5 Merging and rollup
Section titled “2.5 Merging and rollup”Within the one authoritative pass over a function (§2.6), the engine may record the same facet of the same site more than once, for example from two paths in the final pass or from two requirements of one library call. Records merge by rank:
violation > unresolved > checked > trusted > proven
Merging by rank applies only within that pass. LedgerAdapter::beginFunction
discards every row an earlier pass recorded for the function, so a stale
decision from a superseded run never survives (§14).
Each record is kept in the row’s requirements list with its own outcome
(§12.1), and checks are planned per record, not per merged facet: a checked
requirement gets its check even when another requirement of the same site
is unresolved (§10.1). char dst[16]; memcpy(dst, src, n); with an unknown
source extent therefore checks n <= 16 for the destination, while the
merged spatial facet is unresolved(unknown-extent). The site outcome
used by the summary line is the highest-ranked outcome among its facets.
2.6 Completeness by construction
Section titled “2.6 Completeness by construction”The ledger is complete by construction:
SiteCollectorruns before the engine. It walks the body of every emitted function and assigns ordinals. Emitted functions are the definitions, not in system headers, whose body CodeGen may emit in any form: thoseASTContext::DeclMustBeEmittedaccepts, referenced internal-linkage definitions, and C99inlineandgnu_inline(extern inline) definitions, which CodeGen emitsavailable_externallyand inlines at-O1and above. All of them are analysed and instrumented, whether or not they are referenced; ledger rows are kept for the ones that are externally visible or used.- The engine publishes decisions for the sites it reaches (§14). A
decision for an expression that
SiteCollectordid not enumerate is an internal error: a debug-build assertion and a release-buildunresolved(unanalysed)row in the enclosing function. LedgerAdapter::finishfills every undecided facet with its default:
| Facet | Undecided, function analysed | Function over budget |
|---|---|---|
| null | checked (nonnull is always expressible, §10.3) |
checked |
| spatial | checked if the extent is exact from the type or declared, and its terms are constants, const locals and parameters that are never assigned or address-taken, else unresolved(unanalysed) |
the same, with unresolved(budget) |
| temporal | unresolved(unanalysed) |
unresolved(budget) |
| assertion | checked |
checked |
Without engine facts, rule 4 of §10.3 (the term’s places are unmodified since the extent was derived) can only be established syntactically, which is why the default’s terms are restricted.
The ledger’s outcomes come from one authoritative pass per emitted function: the context-insensitive analysis of its body, which is the body CodeGen emits for every caller. It runs after the unit’s callback-slot and recursive-SCC fixpoints, with the unit’s sized-field facts in force (the RFC 0012 second reporting pass is folded into it) and with only the §7.6 invariants that survived (§7.6 says which functions are re-run for that). Every other run (fixpoint rounds, later Houdini rounds, the link step’s refinement rounds before the last) publishes into a discarding adapter.
Context-specialised analyses (RFC 0016 call contexts) never decide the rows
of the function they analyse. They sharpen the summaries callers see, and
they keep their diagnostics: a finding a context run makes in the callee is
reported where v0.10.0 reports it, at the use in the callee, with a note
naming the call, and its ledger link is that call’s Call site in the
caller, whose temporal facet becomes a violation (definite in the context)
or unresolved(may-released) (possible). Departure: the resolutions
say context runs only sharpen summaries. Dropping their diagnostics would
lose catches v0.10.0 makes, such as two(p, p) with
void two(char *a, char *b) { free(a); b[0] = 1; }, against the
resolution that every current catch is still reported (gates G1–G4). The
callee’s own site is not proven either way (§3.1, may-alias-released).
Functions defined in system headers are not in the ledger.
3. Certainty and severity
Section titled “3. Certainty and severity”Every diagnostic is definite or possible. Definite violations are errors. Possible findings are warnings for the temporal ids and produce no diagnostic for spatial and null facets, which become checked instead.
3.1 Temporal certainty
Section titled “3.1 Temporal certainty”core::MoveRecord (include/weavec/Core/Moves.h) has no path-coverage
information today: MoveTracker::join (lib/Core/Moves.cpp) inserts the
other side’s records and keeps set union. This RFC adds three fields:
/// RFC 0030: every predecessor merged since the record was made had it.bool allPaths = true;/// RFC 0030: made from a callee effect that holds only on some outcome/// classes or paths (a `PendingOutcome` class, or a summary effect that is/// not `consumesUnconditionally`). Cleared when a test of the result/// narrows the pending classes to ones that all consume the place, unless/// the effect is `lossy` (§9.1), in which case it is never cleared.bool conditional = false;/// RFC 0030: made by the unknown-callee default (§5.1) or an open slot/// (§9.3). Never diagnosed.bool unknownOrigin = false;MoveTracker::join gains a second loop over this side’s records. The rules:
- A record present on one side only gets
allPaths = false. - A record on both sides gets:
allPaths = a.allPaths && b.allPaths && (a.unknownOrigin == b.unknownOrigin);conditional = a.conditional || b.conditional;unknownOrigin = a.unknownOrigin && b.unknownOrigin.
- The existing rules for reason, location, witness, guard and
ownValueare unchanged.
Amendment (S3). A record of the function’s own value (RFC 0008,
ownValue) on one side and an unknownOrigin record on the other describe
different values: the join is the unknownOrigin record, with
allPaths = false, so releasing a value the function stored itself never
reads as a known release of the caller’s value. Otherwise, when one side
is known and the other unknownOrigin, the joined record takes the known
side’s reason, location and names (only a known record is ever diagnosed),
with the bits and guard joined as above. A record copied to another name (an
assignment, a mirrored heap cell, a record restored after a store) keeps all
of its certainty bits.
A record is definite when
allPaths && !conditional && !unknownOrigin && guard.trivial()where guard is the RFC 0009 PlaceGuard and trivial() means no
conjuncts.
Amendment (S3). Only the callee’s part of the guard counts. A
summary when condition that is still undecided at the call makes the
record conditional, which the rule above already excludes. The path-fact
conjuncts the engine adds when it makes the record held on every path that
made it, so they do not make it indefinite. Pruning them at the use was
tried: numeric predicates are often undecidable there, and plain
free(p); p[0] after a loop became a warning. So the implemented test is
allPaths && !conditional && !unknownOrigin. A second unconditional
consume of a place reaffirms its record (allPaths again, guard = the
facts at the second consume): while (n--) { free(p); use(p); } is a
possible double free and a definite use after free. At a use or release that hits record r
(FunctionDataflow::reportUseOfMoved, the double-free branch of the
consume path):
| Record | Outcome of the temporal facet | Diagnostic |
|---|---|---|
definite, reason Freed/Released |
violation | use-after-free or double-free, error |
definite, reason Moved |
violation | use-after-move (or double-free), error |
definite, reason Uninitialized |
violation of the null facet for a pointer (§3.2); no facet for a scalar (§3.4) | use-of-uninitialized, error |
not definite, not unknownOrigin, Freed/Released |
unresolved(may-released) |
same id, warning, “may” wording |
not definite, not unknownOrigin, Moved |
unresolved(may-moved) |
same id, warning |
not definite, Uninitialized |
null facet checked (zero-initialised) or unresolved(no-zero-init) |
none |
unknownOrigin |
unresolved(unknown-callee) or unresolved(callback) |
none |
| no record, but a may-alias release (below) | unresolved(may-alias-released) |
none |
| no record | proven (under the entry assumptions and §9.4) | none |
A known release after an unknown one. MoveTracker::markMoved keeps
the original record when a place is consumed again. When a consume through
a known effect (a free, a summary’s unconditional consume) hits an
unknownOrigin record, the release’s own temporal facet is
unresolved(unknown-callee) without a diagnostic, because the unknown
callee may already have released the object. The record is then replaced
by the new one (unknownOrigin = false, allPaths = true), so
consume(p); free(p); p[0] = 1; still reports the definite use-after-free
that v0.10.0 reports.
Aliases of a released object. A place with no MoveRecord can still
alias a released object, as in free(b->data); b->cur[0] = 0; when cur
points into data, or free(a); b[0] = 1; when a and b are the same
argument. After a release on some path, the temporal facet of an access
through a pointer loaded from a parameter- or global-rooted place, or
copied from a parameter, is unresolved(may-alias-released) unless the
pointer is provably distinct from every object released so far on some
path. Provably distinct means one of:
- the pointee types could not designate the same object under C’s
effective-type rules: neither is a character type and they are not
compatible (a unit compiled with
-fno-strict-aliasingtreats every pair as compatible); - both pointers were loaded from owning places (§9.4), which the
owner-uniqueness assumption keeps distinct, so
free(s->a); free(s->b)stays proven; - the engine’s place identity shows different allocations or storage;
- the pointer was stored after the release.
The rule adds no diagnostic. A caller that passes aliased arguments is still reported through the context runs (§2.6), as v0.10.0 reports it.
core::Loan (Borrow.h) gains the same allPaths bit, cleared by
BorrowState::join for a loan present on one side only.
conflicting-borrow is an error only when the loan has allPaths and the
conflicting move or release is reached unconditionally from the loan;
otherwise it is a warning, and the move or release’s temporal facet is
unresolved(may-conflict).
The rule is conservative on purpose. A join always clears allPaths, even
when a guard encodes the condition, because guards are bounded
(MaxGuardConjuncts = 8) and can be weakened by dropping conjuncts, which
would make a false definite finding possible. A correlated bug
(if (n == 0) free(p); … if (n == 0) use(p);) is therefore a warning, not
an error.
3.2 Null certainty
Section titled “3.2 Null certainty”core::NullRecord (Nullness.h) gains
bool allocatorSource = false. It is set when the value is the result of a
call whose LibrarySpec entry allocates or whose summary returnsFresh().
It is preserved by copies and tests, and joins by ||. The null facet of a
dereference, and of an argument passed where the callee or the library
entry requires non-null, is decided from the engine’s fact about the
operand:
Fact (NullTracker) |
Null facet | Diagnostic |
|---|---|---|
NonNull (tested, dereferenced before, declared WEAVEC_NONNULL/nonnull/_Nonnull, [static N], or LibrarySpec non-null result) |
proven | — |
Null, not allocatorSource |
violation | null-dereference, error |
Null, allocatorSource |
checked | allocation-failure (off by default) |
MaybeNull |
checked | allocation-failure if allocatorSource |
| no record (parameter, loaded field, unknown result) | checked | — |
possibly uninitialised with -fno-weavec-zero-init |
unresolved(no-zero-init) |
— |
Null is must by construction: the NullTracker::join table turns Null
joined with anything else into MaybeNull. A finding that rests only on a
system-header attribute is never definite and produces no check; the
argument’s null facet is trusted(system-api) (§5.2, §7.2).
Refinement after a dereference. After a dereference whose null facet is
proven or checked, the existing markDereferenced rule makes the
pointer NonNull downstream. This is sound in the enforcing modes, where
the check would have trapped, and the ledger always describes those modes
(§Soundness). A dereference whose null facet is trusted (unsafe,
system-api) or unresolved (no-zero-init) refines nothing, and neither
does a spatial facet that is not proven or checked: an index check bounds
the index downstream only when it was planned. The same holds for an
assumption (§6.2).
Requirements at calls. The engine’s FunctionSummary::requiresNonNull
and requiresExtent are may-facts: noteRequirement records any
dereference of a parameter with no nullness record, on any path. From this
RFC on they feed only summaries and fix-its. The definite
“'<p>', which is null, is passed to '<f>', which dereferences it” error,
call-site checks, and any post-call NonNull or extent fact come only from
the must-access requirements of §7.5, or from a call site whose own
nonnull check is planned. checkRequiredArguments stops calling
markDereferenced on an argument of unknown nullness otherwise: after
f(q, 0) with void f(int *p, int c) { if (c) *p = 1; }, a later *q is
still checked.
3.3 Spatial certainty
Section titled “3.3 Spatial certainty”The engine’s spatial decision is core::checkSpatialBounds, which returns
core::SpatialCheck{outcome, reason, violation} (Spatial.h). It maps to
the ledger as follows, and core::SpatialOutcome/SpatialReason are then
removed:
| Engine result | Facet |
|---|---|
Proven |
proven |
Violation with BoundsVerdict::Kind OutOfBounds, BeforeStart or AtLeastPastEnd, against an exact extent |
violation (out-of-bounds, error) |
| the same, where the only extent is a declared kind (an access inside the body past the declaration) | checked if expressible, else unresolved(inexpressible); never an error, since the object may be larger. An argument whose exact extent is below a declared requirement is a violation at the call, as for any exact extent |
| the same, when the extent rests only on a system-header attribute | trusted(system-api), no check and no diagnostic |
Violation with MayBeOutOfBounds or MayReachPastEnd |
checked if the extent is exact or declared and expressible, else unresolved(inexpressible); no diagnostic |
Unresolved, UnknownIndex (extent exact or declared) |
checked if expressible, else unresolved(inexpressible) |
Unresolved, UnknownIndex against a lower-bound kind, or UnknownExtent |
unresolved(unknown-extent) |
Unresolved, UnknownOffset |
unresolved(unknown-index) |
Unresolved, Arithmetic or UnsupportedExpression |
unresolved(unanalysed) |
Unresolved, InterfaceRequirement |
checked at static call sites when the requirement matches a §7.5 must-access requirement with a trivial when guard; otherwise, inside the body, unresolved(unknown-extent), or trusted(caller-contract) for an exported or address-taken function whose §7.5 requirement covers the access |
A violation needs an exact extent (§7.1). A declared kind is a lower
bound: the object may be larger, so an access past it breaks the
declaration, not necessarily the object. Library-call requirements (§8)
follow the same table, each requirement separately. A definite overlap of a
disjointness requirement is reported as out-of-bounds, with the message
“'<f>' copies <n> bytes between overlapping ranges of '<obj>'”.
3.4 The other ids
Section titled “3.4 The other ids”| Id | Definite (error) when | Otherwise | Facet and possible reason |
|---|---|---|---|
invalid-release |
the released pointer exactly aliases a non-heap object, literal or interior position on every path | warning | Release spatial; may-invalid-release |
mismatched-release |
the resource’s family is exact (not joined from different families) | warning | Release temporal; may-mismatched-release |
lifetime-too-short |
the escaping value exactly aliases the dead storage (exact alias edge) | warning | Call exit temporal; may-dangle |
conflicting-borrow |
as §3.1 | warning | temporal of the move or release; may-conflict |
leak |
never | warning; not reported at a return from main or after an exit-family call (LibrarySpec exits) |
none: a leak is not a facet |
invalid-integer-operation |
unchanged (RFC 0017: already definite-only) | — | none |
unsafe-operation |
unchanged (RFC 0004) | — | the Raw site’s facets |
annotation-mismatch |
always | — | the facet the contradicted declaration or relied-upon fact governs |
invalid-annotation |
never | warning | none |
use-of-uninitialized on a scalar |
as §3.1 | no diagnostic | none: scalars have no facet (§Soundness) |
-Werror in project flags still does not promote WeaveC warnings.
-Werror=weavec[-<id>] does. Errors cannot be disabled, only lowered to
warnings, as today.
Lowered violations still trap. When a definite violation’s error is
lowered to a warning and the unit therefore produces an object,
CheckPlanner guards the site anyway in the enforcing modes. A spatial,
null or assertion violation gets its facet’s check, which traps whenever
the violation actually happens. Any other violation (temporal, a release),
and one whose check is not expressible, gets an unconditional
__builtin_verbose_trap("weavec", "violation") before the operation.
With -fweavec-checks=none nothing is emitted, as for every other check.
This is what makes (V) hold under -Wno-error. Amendment (S3). The ledger adapter
enforces this for every reporting path: a definite error that names a
facet, or whose id governs one (out-of-bounds and invalid-release:
spatial; null-dereference and use-of-uninitialized: null; the temporal
ids; contradicted-assumption: assertion), records a violation on that
facet of its site, or of the innermost site around it when it was reported
at a subexpression that is no site of its own (a call argument whose callee
requires more than it has), adding the facet where the site kind would not
otherwise carry it. The planner then guards that facet when the error is
lowered, including a library call’s facet whose requirement records carry
no check of their own.
4. Worked examples
Section titled “4. Worked examples”Each example gives the source, the ledger outcomes of its interesting
sites, the diagnostic if any, and the emitted check written as equivalent
C. Helper names are defined in §10.2. Every example is also a case under
test/cases/semantics/.
Proven. A declared kind discharges both facets:
int sum4(const int a[static 4]) { return a[0] + a[3]; }a[0] is a Deref site: null proven ([static 4] implies non-null, A1),
spatial proven, temporal proven. a[3] is an Index site with spatial
proven (3 < 4). No code is emitted.
Checked, null. Unknown nullness costs a branch:
struct node { struct node *next; int v; };int second(struct node *n) { return n->next->v; }Both Deref sites have null checked. Spatial is proven, because n is
Single under A1 and the slot node.next is Single (§7.3). Temporal is
proven. Emitted:
return ((struct node *)__weavec_chk_nonnull( ((struct node *)__weavec_chk_nonnull(n))->next))->v;Checked, index. A declared counted pointer:
int at(const int *WEAVEC_COUNTED_BY(n) p, size_t n, size_t i) { return p[i]; }Spatial checked with the index template, null checked, temporal proven. Emitted:
return ((const int *)__weavec_chk_nonnull(p))[__weavec_chk_index(i, n)];Checked, library length. A copy into a fixed buffer:
void greet(const char *name) { char buf[16]; strcpy(buf, name); puts(buf); }strcpy is a LibCall site with two spatial requirements. The destination
requirement (strlen(name) + 1 <= 16) is checked against the exact extent
of buf, computing the length with the bounded prelude helper, which reads
at most 16 bytes. The source requirement, that name is NUL-terminated
within its object, is inferred by R4 (§7.5); greet is exported, so it is
trusted(caller-contract). The merged spatial facet is checked, and its
requirements list both. The null facet of name is checked: nullability
is never left to a caller contract. name is side-effect free, so the
terms are expressible:
(__weavec_chk_len(__weavec_strnlen(name, sizeof buf) + 1, sizeof buf), strcpy(buf, (const char *)__weavec_chk_nonnull(name)));__weavec_strnlen traps on a null pointer itself, so the length term
never reads through null before the argument’s own check runs.
Violation, spatial.
void f(void) { char *p = malloc(4); if (!p) return; p[4] = 0; free(p); }p[4]: spatial violation (OutOfBounds), reported as
error: 'p[4]' is out of bounds: … an object of 4 bytes [weavec::out-of-bounds].
No object is produced.
Violation versus possible, temporal.
void g(char *p) { free(p); p[0] = 1; } // error: use of 'p' after it was freedvoid h(char *p, int c) { if (c) free(p); p[0] = 1; } // warning: use of 'p' after it may have been freedIn h, the record for p reaches p[0] through a join with a
predecessor that lacks it, so allPaths is false. The temporal facet is
unresolved(may-released) with a warning. The null facet is checked, and
the program builds.
Violation, null.
int k(struct node *n) { if (n == NULL) return n->v; return 0; }On the null edge, n is Null with reason Tested, not from an
allocator. The result is
error: dereference of 'n', which is null [weavec::null-dereference].
Unresolved, unknown extent.
char *get_buffer(void); /* no definition, entry or annotation */int f(void) { char *b = get_buffer(); return b[1000]; }b is Single-or-nullable under A3 (§7.3). Single is a lower bound, so
element 0 is covered but element 1000 has no extent, and no check is
written against the lower bound. b[1000]: spatial
unresolved(unknown-extent), null checked, temporal proven. The call is a
Call site whose temporal facet is unresolved(unknown-callee), with a
fix-it suggestion in the ledger.
Unresolved, unknown callee.
void consume(char *p);int f(void) { char *p = malloc(8); if (!p) return 0; consume(p); return p[0]; }p[0]: temporal unresolved(unknown-callee), because consume may have
freed p. There is no diagnostic. If a unit that defines consume and
frees p is linked with weavec-cc, the link step reports
use-after-free as an error and the link fails (§13).
Unresolved, inexpressible.
struct buf { char *WEAVEC_COUNTED_BY(cap) data; size_t cap; };char get(struct buf *b, struct buf *other, size_t i) { char *d = b->data; b = other; /* the extent was 'b->cap' of the old 'b' */ return d[i]; /* spatial: unresolved(inexpressible) */}The engine still knows the extent, as a snapshot place, but it has no C
name at d[i].
Trusted, unsafe.
void poke(uintptr_t addr) { WEAVEC_UNSAFE { volatile unsigned *reg = (volatile unsigned *)addr; *reg = 1; }}The IntToPtr site’s spatial and temporal facets are trusted(unsafe).
reg is an RFC 0004 raw pointer (cast from an integer), so *reg is a Raw
site, and its spatial, null and temporal facets are all trusted(unsafe).
No checks are emitted. Outside the region, *reg would be an
unsafe-operation error, as today.
Trusted, system API. ioctl has no LibrarySpec entry, because the
layout of its third argument depends on the request:
int cols(int fd) { struct winsize ws; if (ioctl(fd, TIOCGWINSZ, &ws) == -1) return 80; return ws.ws_col; }The ioctl call is a Call site with temporal trusted(system-api). The
address of ws is borrowed for the call.
Trusted, concurrency. In probe 15, worker is a pthread_create
target and frees the global shared. main’s later shared[0] has
temporal trusted(concurrency), and so does every access to shared
inside worker; the summary line counts them under A4. A null test of
shared in main followed by a use does not prove the use: its null facet
is checked (§5.3).
Assumptions.
int a(char *b, int n) { WEAVEC_ASSUME(n > 0); return b[n - 1]; } // assertion: checkedint c(void) { char b[4] = {0}; int i = 10; WEAVEC_ASSUME(i < 4); return b[i]; }In a the assertion is not proven, so it is emitted as
__weavec_chk_assert((n > 0) != 0). In c the assertion is refuted:
error: assumption 'i < 4' is false here [weavec::contradicted-assumption].
Require levels. Under -fweavec-require=checked, the unknown-extent
example above fails with
error: access 'b[1000]' is neither proven nor checkable: the extent of 'b' is unknown [unknown-extent] [weavec::unresolved-operation].
Under -fweavec-require=proven, second above also fails with two
unchecked-operation errors.
5. Sound defaults for code WeaveC cannot see
Section titled “5. Sound defaults for code WeaveC cannot see”5.1 Unknown callees
Section titled “5.1 Unknown callees”A direct callee is unknown when all of the following hold:
- it has no body in the TU;
- it has no summary from the program database (only available at link);
- it has no
LibrarySpecentry; - it is not declared in a C library, POSIX or platform header (§5.2).
The unknown-callee default applies per pointer parameter. A pointer
argument whose parameter has no ownership contract receives the effects
below. An ownership contract is a WeaveC ownership annotation
(WEAVEC_OWNED, WEAVEC_BORROWED, WEAVEC_MUT, WEAVEC_RAW,
WEAVEC_RELEASES, WEAVEC_RETAINS) or an ecosystem ownership attribute
(ownership_takes, ownership_holds, and malloc/ownership_returns for
the result). Nullability and extent attributes (nonnull, _Nonnull,
counted_by, …) never lift the temporal default: they only decide null and
spatial facets. A callee whose pointer parameters all carry ownership
contracts applies those contracts, and its Call site’s temporal facet is
trusted(extern-contract). Departure: the resolutions define an
unknown callee as one with “no declared contract”. Read as “any attribute”,
that let a destructor declared only __attribute__((nonnull)) act as
borrow-only, which is v0.10.0’s unsoundness again; only ownership contracts
count.
For each argument without an ownership contract, a call to an unknown callee:
- marks the argument’s place, every pointer-typed place reachable through
a non-
constpointee of it, every escaped place and every pointer-typed global that external code can reach (external linkage, or address taken) with aMoveRecordof reasonFreed,unknownOrigin = trueandallPaths = false; - forgets the nullness, extents and scalar facts of the places reachable
through its non-
constpointees, of escaped places and of reachable globals. The argument value’s own nullness and extent are kept: the callee cannot change the caller’s copy of the pointer or the size of the object, only its lifetime, which the record covers.
The result gets an unknown nullness, the Single kind (A3) and no ownership, so it is not tracked as a leak.
A later use of a marked place is unresolved(unknown-callee) without a
diagnostic. So is a later release: the callee may have retained the value.
The Call site’s temporal facet is unresolved(unknown-callee) and carries
a fix-it suggestion. For example: “declare ‘consume’ with WEAVEC_BORROWED
on ‘p’ if it neither keeps nor frees it”. The per-call
annotation-required warning is removed. An indirect call through an open
slot with no known target (§9.3) behaves the same way with reason
callback.
In a per-TU compile, calls into other units of the same program are unknown. The link step (§13) replaces these defaults with the definitions’ summaries.
Amendment (S7): the default is recorded lazily. Marking every place reachable below the argument is both unsound and expensive, and S7 replaces it with one record per object.
The unsoundness: the marking can only reach the places the analysis has
interned when the call is transferred. A place the function names only
afterwards — struct mid *m = o->m; consume(o); m->in->v, where m->in
is interned by the last statement — gets no record and its temporal facet
is proven. That is a false proof, and a trap-mode build omits a check
because of it. The case is test/cases/semantics/aliasing/unknown-callee-later-named.c.
The representation. A call to an unknown callee marks, as before, the
argument’s holders, every escaped place and every reachable global, and it
marks the object the argument points at. It does not walk below that
object. A place with no record of its own inherits the nearest record
of unknown origin above it, following the place tree upwards and the exact
aliases of each place on the way (m ~ o->m carries o’s record to
m->in). Two things stop the walk:
- a place this function has given a value to since the record was made
(
consume(o); o->m = malloc(n); o->m->in), which is kept as a small per-state set of places, joined by intersection so that a place only one path gave a value to inherits again; - a record of known origin, which speaks for its own place: the access through it is reported there.
A name outside the object for a pointer inside it (char *q = o->m->in;
before the call) does not lie below the object, and the forgetting pass
separates it from the inside name, so those few names are still marked
where the pass finds them.
Why this is the same or stronger. For a place that exists at the call and is not written afterwards, the inherited record has the same reason, position, origin and certainty bits as the eager one, so every facet, every diagnostic and every summary effect is the one the eager rule gives. For a place the function writes afterwards, both rules agree that it is this function’s again — eagerly by erasing the record, lazily by the stop set. For a place first named after the call, the eager rule proves the facet and the lazy one does not: strictly stronger, and the only outcome that changes. Non-pointer places inherit nothing, as the eager rule marked none.
What it costs. One record instead of one per reachable place: in a large
function with many calls into code it cannot see, the records were most of
what a state carried, and a state is copied per CFG edge. Summaries export
the roots (PlaceEffect::unknown on the object’s path) rather than hundreds
of paths below them, which is what §13.1’s records carry across units and
what applyUnknownEffects already reads, for the same reason: a parameter
whose own value is unknown covers every path below it.
5.2 System APIs outside the table
Section titled “5.2 System APIs outside the table”A C library, POSIX or platform header is a system header
(SourceManager::isInSystemHeader) that is either under the toolchain’s
resource directory, or under the SDK or sysroot and named, relative to its
include directory, in the LibrarySpec header list (stdio.h,
sys/ioctl.h, pthread.h, …; on Darwin every header of the SDK counts).
The same definition decides which link inputs are system (§13.2).
Third-party libraries installed as system headers (jansson.h in
/usr/include or /opt/homebrew/include, sqlite3.h) are not in the list,
so their functions are unknown callees (§5.1). Departure: the
resolutions trust every system-header function. Under that rule
json_decref(j); json_string_value(j) against an installed jansson would be
proven, so the trust is limited to the platform’s own headers.
A function declared in a C library, POSIX or platform header without a
LibrarySpec entry:
- borrows its arguments for the call, with no release, retain or store effect;
- returns a Single-or-nullable result of unknown nullness;
- puts
trusted(system-api)on the Call site’s temporal facet.
Later uses are unaffected, and are proven under A2. Clang attributes on
such declarations (for example glibc’s __nonnull) never make a finding
definite and never produce checks. They record trusted(system-api) on
the argument’s null facet when the argument is not proven non-null. The
system-header exemption in FunctionDataflow’s boundary check
(!options.reportUnannotated && sm.isInSystemHeader(...)) is removed:
every such call is in the ledger.
5.3 Callbacks, threads and signals
Section titled “5.3 Callbacks, threads and signals”LibrarySpec callback clauses (§8) say how a function-pointer argument
is invoked.
-
sync(qsort,qsort_r,bsearch,lfind,lsearch,tsearch,tfind,tdelete,twalk,scandir): at the call, the target’s summary is applied as a may-effect. The may-effect covers zero or more invocations, with parameters bound to borrowed pointers into the argument the clause names and to the context argument. The target is resolved from the argument’s function value or its slot (§9.3). An unresolvable target gets the unknown-callee default with reasoncallback. Probe 47 thereby becomes a possible use-after-free warning. -
entry(pthread_create,thrd_create,signal,sigaction,pthread_atfork): the target is analysed as an entry point, as every emitted function already is. Define G = the pointer-typed globals read, written or released by any function reachable from an entry target (call graph plus solved slots), together with every object reachable from the argument passed to the target. In every function, entry targets and the functions they reach included, a facet whose pointer is loaded from a place rooted in G is treated like asetjmpfunction’s facets (§5.4):- its temporal facet is
trusted(concurrency); - a null or spatial facet that only flow facts would prove (a test
followed by a use, which another thread or a handler can invalidate in
between) is checked instead, because the check tests the value the
access actually uses; when the check is not expressible, the facet is
trusted(concurrency); - facets proven by type alone (a constant index into an array member) stay proven.
Departure: the resolutions put only the shared globals’
trusted(concurrency)on record. Applying it to temporal facets only, and only outside the entry targets, leftif (g_buf) g_buf[0] = 1;racing with a handler that setsg_buf = NULL, and the worker’s own use of an objectmainfrees, proven. - its temporal facet is
-
at-exit(atexit,at_quick_exit,on_exit): the target is analysed as an entry point. It does not contribute to G, because it runs after the main flow has ended. A dangling global left for it is caught instead by the exit boundary at theexitcall (§9.4).
5.4 setjmp
Section titled “5.4 setjmp”In a function that calls a returns-twice function (setjmp,
sigsetjmp, _setjmp, or a declaration with Clang’s returns_twice
attribute):
- every temporal facet is
unresolved(setjmp); - every spatial or null facet that would be proven from flow facts about
non-volatile locals is downgraded to checked, or to
unresolved(setjmp)when not expressible. Facets proven by type alone (a constant index into an array) stay proven.
longjmp and siglongjmp are noreturn in the table.
5.5 Budgets
Section titled “5.5 Budgets”Each run of FunctionDataflow on one function body counts the CFG blocks
it transfers, both in the fixpoint and in the final pass. The count is
deterministic and does not depend on timing.
- When the default-context analysis of a function exceeds the budget, it
stops. The function’s facets take the default outcomes of §2.6 with
reason
budget. Its summary’s effects become the unknown-callee effects (§5.1), so callers stay sound; its result keeps the syntactic kind of §7.3. - The context-specialised analyses of one function share a second budget of the same size, counted over all of its contexts together. Once it is exhausted, further contexts use the default-context summary. This bounds the context runs that dominate Lua’s analysis time (gate G15).
- The summary line and the ledger list over-budget functions.
A summary the engine marks incomplete (the 32
inferred.incomplete.insert(…) reasons, SummaryStore::markIncomplete) or
produces when MaxFixpointRounds is exhausted may under-approximate the
callee. Callers apply its known effects plus the unknown-callee may-effects
of §5.1 on every pointer argument, and its result keeps only its §7.3 kind.
The function’s own sites that the engine did not decide take the defaults
of §2.6 with reason unanalysed.
The budget is set by -fweavec-budget=<n> (--budget=<n> in weavec);
0 means unlimited. The initial default is 200,000 block transfers. S3
calibrates it once: the final default is the smallest multiple of 50,000
that is at least four times the 99.9th percentile of per-function counts
over the corpus. The chosen value is recorded in the CLI reference. At most
1% of corpus functions may exceed it (gate G15). This budget replaces the
internal “iteration limit reached” incompleteness paths. The existing
MaxFixpointRounds guard stays as a bug guard.
Correction (S3). The calibration measured 2,874 corpus functions (every
config’s files per unit and the whole-program configs as programs, budget
unlimited): the 99.9th percentile is 7,208 block transfers and the largest,
Lua’s luaV_execute, 25,518, so the default is 50,000 and no corpus function
exceeds it. A run that hits MaxVisitsPerBlock has no fixpoint and counts as
over budget. A summary whose incompleteness is about integers or extents,
about a context run the callee fell back from (the default-context summary is
sound), or about an indirect call with an unresolved target (the callee
applied §5.1 there, and its summary’s unknown effects say what it handed
on) leaves the effects complete; the other reasons, the round limit and the
budget make callers apply the unknown-callee default at the call.
5.6 Headers
Section titled “5.6 Headers”-fweavec-analyze-headers is removed. Every emitted function (§2.6) is
analysed and instrumented, including static inline functions from user
headers. Each TU instruments its own copy. Definitions in system headers
are never analysed or instrumented.
5.7 Inline assembly
Section titled “5.7 Inline assembly”An asm statement with pointer-typed operands applies the unknown-callee
default (§5.1) to those operands, with detail “inline assembly”. An asm
statement with a "memory" clobber also applies it to every escaped place
and to every reachable global.
6. WEAVEC_UNSAFE, WEAVEC_ASSUME and require levels
Section titled “6. WEAVEC_UNSAFE, WEAVEC_ASSUME and require levels”6.1 WEAVEC_UNSAFE
Section titled “6.1 WEAVEC_UNSAFE”Sites inside an unsafe region (a function or block annotated
weavec.unsafe):
- Spatial and null facets are
trusted(unsafe), and no checks are emitted. A definite spatial or null violation is still an error. - Temporal state is tracked exactly as outside the region. A definite
temporal violation is still an error (probe 45). A possible temporal
finding is reported exactly as outside the region: a warning with “may”
wording, and
unresolved(may-released)orunresolved(may-moved). - Raw sites (§2.1: pointers with an RFC 0004 raw origin, including those
converted from integers) and IntToPtr sites inside the region are
trusted(unsafe)for every facet, temporal included; this is the only temporal use of the reason. Outside a region, a Raw site remains anunsafe-operationerror (RFC 0004). - An Assume site inside the region keeps its assertion facet, and its check is emitted: the region trusts raw memory operations, not assumptions.
- A trusted dereference inside the region refines nothing after it
(§3.2): the pointer is not
NonNulldownstream because of it.
The inUnsafe suppression (in FunctionDataflow::report and about 22
other sites across 7 files) is replaced by these rules; no diagnostic is
dropped for being inside a region. What the region does to the surrounding
code (a free, a store) is still visible there, as RFC 0004 specifies.
6.2 WEAVEC_ASSUME(e)
Section titled “6.2 WEAVEC_ASSUME(e)”The Assume site’s assertion facet is:
- proven when the engine proves
eat the site. The call stays asweavec.hwrote it: a no-opweavec_assume_, which the optimiser removes. - violation when the engine refutes
e. This is thecontradicted-assumptionerror. - checked otherwise.
CheckEmitterreplaces the call with__weavec_chk_assert((e) != 0). With-fweavec-checks=nonethe call is left alone.
In every case the analysis assumes e after the site, as today. That is
sound in the enforcing modes, because the program would have trapped
otherwise, or the violation fails the build (or traps, §3.4). The header’s
weavec_assume_ keeps its no-op body, so the behaviour is the same with
any compiler and in every WeaveC mode except the rewrite itself. The
header’s doc comment for WEAVEC_ASSUME, which today says it is “trusted
like every annotation”, is rewritten to describe these three outcomes.
6.3 Require levels and WEAVEC_REQUIRE_SAFE
Section titled “6.3 Require levels and WEAVEC_REQUIRE_SAFE”-fweavec-require=none|checked|proven (default none; --require in
weavec) makes the ledger fail-closed:
| Level | Error for each facet that is | Id |
|---|---|---|
none |
— | — |
checked |
unresolved | unresolved-operation |
proven |
unresolved; checked | unresolved-operation; unchecked-operation |
Trusted facets are allowed at every level: trust is explicit and listed.
The dangling-escape and second-owner reasons are therefore errors only
under checked or proven.
WEAVEC_REQUIRE_SAFE, before a function definition, holds that function’s
sites to checked whatever the command line says. It replaces
WEAVEC_CHECKED. -fweavec-require=proven is the only fail-closed tier
without runtime checks, and replaces --checked.
7. Pointer kinds and where spatial obligations sit
Section titled “7. Pointer kinds and where spatial obligations sit”7.1 The lattice
Section titled “7.1 The lattice”namespace weavec::core {enum class PointerShape : std::uint8_t { Single, Counted, Sized, EndedBy, NulTerminated, Unknown };enum class Nullability : std::uint8_t { Nonnull, Nullable };enum class KindSource : std::uint8_t { Declared, Inferred, Default };/// k * <path> + c, or the constant c; the path is a sibling parameter/// (`param <i>`) or a sibling field (`.<field>`) of the same object.struct ExtentTerm { std::optional<SummaryPath> path; std::int64_t scale = 1; std::int64_t offset = 0; };struct PointerKind { PointerShape shape = PointerShape::Unknown; ExtentTerm extent; // Counted: elements; Sized: bytes; EndedBy: the end pointer's path Nullability nullability = Nullability::Nullable; KindSource source = KindSource::Default;};}The meaning of each shape, for a value p of type T * (as a guarantee
about values, or as a requirement on arguments):
| Shape | Meaning when p is not null |
|---|---|
Single |
at least one T is accessible from p (at least sizeof(T) bytes, 1 for void) |
Counted(e) |
at least e elements of T are accessible from p |
Sized(e) |
at least e bytes are accessible from p |
EndedBy(q) |
p <= q, and [p, q) lies in one object |
NulTerminated |
a zero element lies at or after p within p’s object |
Unknown |
nothing |
“At least” is deliberate: a kind never claims the object ends there. That makes every kind a lower bound, and it gives one rule for what a check may compare against. An exact extent is one of:
- the object type of an array or VLA object (the VLA’s extent term is
sizeof vla / sizeof *vla, which never goes stale when the bound variable changes); - an allocation-size value in scope (§7.4, Arithmetic);
- a surviving §7.6 field invariant, which is exact by construction.
A check operand is either an exact extent or a declared kind, which the
programmer asserted. Checks against exact extents are never tighter than
the object. Checks against declared kinds enforce the declaration and may
be tighter than the allocation, which is the point of declaring them.
Lower-bound kinds (Single, the A1 and A3 defaults of §7.3, and §7.5
requirements inferred inside the body) discharge the accesses they cover
and nothing else. Every other access through them is
unresolved(unknown-extent), never checked against the bound: checking
const struct T *a = pa; in a qsort comparator against pa’s one-byte
Single default would trap every correct comparator. Only an exact extent
can make an access a violation (§3.3).
The join (merge of guarantees at a CFG merge or across stores):
x ⊔ x = x;- two kinds with different shapes or different extent terms join to
Unknown. The exception isSingle ⊔ Counted(e) = Singlewhene ≥ 1is proven at the join; Nullable ⊔ Nonnull = Nullable.
Requirements combine by conjunction: every requirement is checked. The
spelling in summaries and records is single, counted(<term>),
sized(<term>), ended-by(<path>), nul-terminated or unknown,
followed by nonnull or nullable. Terms are spelled as in the RFC 0011
extent grammar.
7.2 Declared kinds
Section titled “7.2 Declared kinds”AttributeReader reads these sources; each yields a declared kind:
| Source | Applies to | Kind |
|---|---|---|
WEAVEC_COUNTED_BY(n) / WEAVEC_SIZED_BY(n) |
parameter, field | Counted(n); for void * and character pointees Sized(n). The two macros are synonyms: WEAVEC_SIZED_BY keeps its RFC 0011 meaning, which is elements (bytes for void *), unlike Clang’s byte-counting sized_by. New code should use WEAVEC_COUNTED_BY |
WEAVEC_ENDED_BY(q) |
parameter, field | EndedBy(q) |
WEAVEC_STRING |
parameter, field, result | NulTerminated |
WEAVEC_NONNULL / WEAVEC_NULLABLE |
parameter, field, result, variable | nullability |
counted_by(n), counted_by_or_null(n), sized_by(n), sized_by_or_null(n) |
field (and parameter where Clang accepts it) | Counted(n) / Sized(n), nonnull unless _or_null |
alloc_size(i[, j]) |
function | result Sized(arg i [* arg j]) |
nonnull, nonnull(i, …), returns_nonnull, _Nonnull, _Nullable |
function, parameter, result | nullability |
malloc, ownership_returns(m), ownership_takes(m, i), ownership_holds(m, i) |
function | fresh result of family m; releases or retains argument i |
T p[static N] |
parameter | Counted(N), nonnull |
T p[n] with n an earlier parameter (VLA parameter) |
parameter | Counted(n), nullable |
A constant T p[N] without static is not a requirement, because C gives
it none and code commonly passes fewer elements. It only yields a fix-it
suggestion.
The WEAVEC_* extent macros are resolved by name: the argument is the name
of any sibling parameter (in any position) or sibling field. This works on
GCC, and without Clang’s rule that the count must be declared before use.
An unresolvable name is an invalid-annotation warning, and the kind is
dropped.
Precedence, from strongest:
WEAVEC_*annotations;- ecosystem attributes in non-system code;
- the
LibrarySpecentry; - attributes in system headers.
A conflict at one level is an invalid-annotation warning, and the weaker
kind is used. Findings that rest only on level 4 are never definite and
never produce checks: their facets are trusted(system-api).
A kind declared on a declaration of a function defined elsewhere is checked
at every call site in this TU (declared requirements are checked
everywhere). Inside the definition it holds under A1. At link, every
definition is verified against every declaration any unit saw
(annotation-mismatch, §13).
7.3 Defaults and slot kinds
Section titled “7.3 Defaults and slot kinds”Parameters, results and fields without a declared kind get these defaults.
Parameters.
- A parameter of a
staticfunction whose address is never taken gets the join (§7.1) of the argument kinds at all its direct calls, judged by the Single-valid rules below or by an engine proof of at least one element. A pointer-arithmetic argument that is neither makes the kindUnknown. No assumption is involved. A must-access requirement (§7.5) checked at every call still covers the accesses it names. A static function with no direct call keeps the A1 default. - Every other parameter is Single-or-nullable by A1, unless must-access
inference (§7.5) gives more.
KindInferencerecords a per-parameter flag,reliesOnSingle, when some access in the body, or some store of the parameter into a slot (below), is proven only by that default. - A call in the unit to such a function, whose argument for a
reliesOnSingleparameter is neither Single-valid nor proven to have an element, gets a spatial facet on its Call site:unresolved(unknown-extent), never checked, because a correct program may pass a one-past-the-end value the callee never dereferences on that path. The flag is exported in the unit record, and the link step does the same for calls between units (§13.2). What no unit can see stays under A1. - The parameter
argvofmain(int argc, char **argv[, char **envp])has the declared kindCounted(argc + 1), nonnull, as long asargcandargvare never assigned. Eachargv[i]withi < argcisNulTerminatedand nonnull, andargv[argc]is null; facets resting on these aretrusted(system-api). Amendment (S6):argvitself is seeded as a declaredCounted(argc + 1)(its indexing is proven or checked like any declared kind); the string requirement of anargv[i]argument and the spatial facet of*argv[i]aretrusted(system-api), while the null facet ofargv[i]stays checked, sinceargv[argc]is null.
Results. Results of calls to functions outside the TU (unknown, system or declared-only) are Single-or-nullable by A3. For functions in the TU, the result kind is inferred as the join over all return values, by the Single-valid rules.
Fields and globals (slots) get their kind from KindInference before
the engine runs, as a greatest fixpoint over every store in the TU. No
engine facts are involved, so there is no circularity. Every slot starts as
Single. A store demotes its slot to Unknown unless the stored value is
syntactically Single-valid:
- null;
&object, or an array decaying to its first element;- an allocation whose
LibrarySpecextent is at leastsizeof(T)with constant operands (malloc(sizeof *p),calloc(1, sizeof *p)); - a parameter that is never assigned, incremented or address-taken in the
function. Storing it sets the parameter’s
reliesOnSingleflag, so a caller that passes a cursor gets the Call-site row above; - a call result whose kind is
Single; - a load from a slot that is still
Single.
Stores the syntax does not show also demote:
- a store through an lvalue
*qof pointer typeT *demotes every address-taken slot of a type compatible withT *; a store through avoid **orchar **lvalue demotes every address-taken pointer slot; - a slot whose address is passed to a callee is
Unknown, unless the callee’s summary shows only Single-valid stores through that parameter; - a byte-wise write into an object (a
LibrarySpecargument withworrwaccess, a character-typed store,fread,read) demotes every pointer field of the object’s type.
Demotion repeats until nothing changes. Pointer arithmetic results and
conversions from integers are never Single-valid, so storing a cursor
demotes its slot. A slot with a surviving §7.6 invariant is Counted or
Sized instead. The unit record exports each slot’s kind and the stores
that demoted it; the link step reports a header struct whose slot is Single
in one unit and demoted in another as unverified under A3, and uses the
demoted kind. Stores by units without records are covered by A3.
Locals carry their value’s facts flow-sensitively (the engine’s
SpatialTracker); they have no slot kind.
A Single pointer guarantees one element, not “exactly one”. p[i] with a
non-zero i on a Single pointer is unresolved(unknown-extent) unless an
exact extent or declared kind gives one. It is never checked against the
Single bound, and never a violation.
7.4 Where spatial obligations sit
Section titled “7.4 Where spatial obligations sit”Objects and extents. (S) speaks of “the object the pointer was derived
from”. It is defined as follows, and the engine’s extent computation
(DataflowDynamicExtents, which today takes min(fixed, tail) for a fixed
trailing array) changes to match (§15):
- A trailing array member is flexible when
-fstrict-flex-arraysallows it; at the default level 0 that is every trailing array, whatever its declared bound. Its extent is the rest of the allocation when that is known, elseunresolved(unknown-extent); it never comes from the declared bound. Lua’sTValue upvalue[1]andUpVal *upvals[1], indexed up tonupvalues, are flexible. - A pointer made by
&member,&a[i]or the decay of a sub-array has the extent of the complete object, as__builtin_object_sizemode 0 does.memset(&s->first, 0, sizeof *s), a flat walkint *q = &m[0][0]; for (k = 0; k < 12; k++) q[k] = 0;, and amemcpyinto(char *)&ts->contentsfor Lua’s short strings are all in bounds. - Two cases use the sub-object’s bound. A direct subscript of a
non-flexible array-typed lvalue (
m[i][j]needsj < 4forint m[3][4]) uses the array’s own bound, as C does. The destination of astr*-familyLibrarySpecrow uses the member’s bound, as_FORTIFY_SOURCE=2does. - The object width of a struct type
Tissizeof(T), or the offset of its flexible array member when it has one. It is the width a Single pointer toTguarantees and the width a dereference needs.
Arithmetic. Extents and offsets are compared as mathematical integers,
not in wrapping size_t arithmetic. An allocation’s extent is the byte
count the program actually passed, a size_t value. It may be rewritten as
an element count (bytes / sizeof(T), or a symbolic n for malloc(n * sizeof(T))) only when the multiplication is proven not to wrap. Otherwise
a check compares the byte offset, computed with overflow checking (§10.2),
against the byte value. In probe 06 (unsigned bytes = n * 4u; int *a = malloc(bytes); for (unsigned i = 0; i < n; i++) a[i] = 0;), i < n does
not prove a[i]; the check index(i, bytes / 4) traps, and the probe
carries an exact TRAP: index marker.
-
Dereferences (
*p,p->f,p[0]). The spatial facet is discharged byp’s kind or local facts:- proven when
pis Single, or has a kind that guarantees at least one element; - checked with
index(0, e)when the kind is a declared or exactCounted(e)ande ≥ 1is not proven; - checked with
spanwhenpis a cursor whose base and extent are exact and expressible; - otherwise
unresolved(unknown-extent)orunresolved(unknown-index).
- proven when
-
Indexing (
p[i],a[i],*(p ± i)) needs0 ≤ i < count: proven, checked withindexagainst an exact or declared count, a violation against an exact count, or unresolved. -
p + i,p - i,++p,&p[i]and&p->fare never checked by themselves. One-past-the-end values are legal. Arithmetic beyond one past the end is undefined in C, but it is not an access, so it is not part of the guarantee; the value is checked where it is used. -
Creation points carry an obligation only where the created value must satisfy a kind, that is, when it flows directly into a required position: an argument for a parameter with a declared kind, or with an enforced inferred requirement (§7.5); a store into a slot with a declared kind; or a
returnfrom a function with a declared result kind.- A PtrArith result in a required position must satisfy the kind.
- A Cast to a wider pointee type in a required position must leave at
least the object width of
Ufrom the pointer. Outside required positions a conversion carries no obligation: its result gets kind Single when the source’s extent (exact or lower bound) covers the object width ofUfrom the pointer, and otherwise Unknown, or a cursor with the source’s base when that base is exact. The obligation then sits at the dereferences. So(struct s *)malloc(sizeof(struct s))is Single, an end sentinel(struct S *)(buf + len)compiles without a trap, and a flexible-array allocation ofoffsetof(struct S, data) + 3bytes is enough forp->a. - An IntToPtr site is
unresolved(raw-cast), ortrusted(unsafe)inside a region. A null pointer constant is not a site.
Departure: the resolutions place an obligation at every cast to a larger pointee type. Checking at the conversion trapped correct code (end sentinels built by casting, flexible-array structs smaller than
sizeof), so outside required positions the obligation moves to the uses, as it does for pointer arithmetic. -
Stores into unannotated slots are not checked. A store that is not Single-valid makes the slot’s inferred kind
Unknown(§7.3), which moves the cost to the slot’s uses. This is what letss->end = s->buf + capcompile without a false trap. -
Library calls check each buffer argument against the entry’s requirement (§8).
-
Grouping. Stores that update a pointer and its count, per a declared or inferred counted relation, form a group when they occur in one basic block with no intervening call, loop or access through the object. This follows
-fbounds-safety. The obligation is checked once, after the last store of the group. Between the stores, accesses use local facts, not the relation.
7.5 Parameter must-access inference
Section titled “7.5 Parameter must-access inference”KindInference computes a requirement for each pointer parameter p. It
uses only the function’s CFG and dominator and post-dominator trees; the
engine is not involved.
A must-access is an access that executes on every path from entry to normal exit, provided that:
- its block post-dominates the entry, or it is inside a canonical counted loop (below) that does;
pis not assigned before it;- no earlier branch tests
p’s nullness; - no path reaches it after an early
return,gotoout of the loop, or call to anoreturnfunction; - every call on a path from entry to it, including calls in the loop body
before it, is known to return: a
LibrarySpecentry withoutnoreturnorexits, a function in the TU whose summary says it always returns, or a function in a C library, POSIX or platform header without anoreturnattribute. Unknown and indirect callees, and afatal()-style helper whose summary says it may not return, disqualify the access.
Rules:
-
R1. A must-access
*p,p->forp[0]givesSingle, nonnull. -
R2. In a canonical counted loop
for (i = c; i < e; ++i)whose body must-accessesp[i]orp[i + k], the requirement isc < e → Counted(e + k)and, for the nullability,c < e → nonnull. The loop is canonical when:c ≥ 0is a constant;eis an affine term over parameters;- the body contains no
break,returnorgotoout; i,eandpare not otherwise modified in the body.
i != ecounts the same asi < e, andi <= egives the guardc <= eandCounted(e + k + 1). The guard is part of the requirement, because a loop that runs zero times accesses nothing:f(NULL, 0)meets the requirement offor (i = 0; i < n; ++i) p[i + 1] = 0;. A requirement that evaluates to zero or less elements is empty. -
R3. A canonical loop
for (x = p; x < q; ++x), orwhile (p < q) … p++, that must-accesses*xgivesp < q → EndedBy(q)andp < q → nonnull.x <= qgivesp <= q → EndedBy(q + 1). -
R4. A canonical scan
while (p[i]) i++,while (*s) s++orfor (; *s; ++s), or a must-call passingpto aLibrarySpecargument that requires a string, givesNulTerminated. -
R5. An unguarded must-access
p[e]withean affine term over parameters givesCounted(e + 1). -
Anything else gives no requirement.
Requirement terms are evaluated as mathematical integers: e + k with a
negative k and an unsigned e does not wrap to a huge count, and a
result of zero or less means no requirement. At a call, the guard and the
terms are checked with the overflow-checked term arithmetic of §10.2.
Requirements are therefore necessary: a correct caller never violates one
in the covered cases.
Enforcement depends on the function and on the part of the requirement.
The nullability part is never trusted. In every body, the null facet of
a must-access dereference of a parameter stays as §3.2 decides it (checked
unless proven), because nonnull is always expressible:
- Static functions whose address is not taken. At every direct call
site, the Call site gets a spatial or null facet for the argument:
proven, checked (
nonnull,len,span, each under the requirement’s guard), a violation (an error, as in probe 09), orunresolved(unknown-extent)when the argument’s extent is unknown. Inside the body the requirement holds, so the accesses it covers are proven, null facets included. A definite violation at a call is reported only when the must-access holds under every rule above. - Exported or address-taken functions. Inside the body, accesses
covered only by an inferred extent requirement beyond Single (
Counted,Sized,EndedBy,NulTerminated) aretrusted(caller-contract). Their null facets stay checked. The requirement is exported in the record, verified at link against every caller the program contains, and offered as a fix-it annotation.
Declared kinds are enforced at every call site whatever the linkage.
Amendment (S6). The implementation (KindInferenceMustAccess.cpp,
lib/Analysis/KindSeeding.cpp) settles these points:
- Covered accesses. A requirement covers the must-accesses that give
it, and nothing else; the other accesses in the body are decided by the
parameter’s entry extent. An unguarded requirement, or a guarded count
that is zero or less whenever its guard fails (
0 < n → Counted(n)), is also the parameter’s lower-bound extent at entry in a static function. A parameter with a declared kind is ruled by its declaration (§7.2): the calls check that kind and not the inferred requirements, so a covered access is proven only where the requirement is the declared kind (same shape and extent term). A body that loops to another bound over aWEAVEC_COUNTED_BY(n)parameter keeps its owncheckedindex. - R1 through a trailing member. A must-access
p->a[k]of a trailing (flexible) member array whose element reaches past the object width givessized(offsetof(a) + (k + 1)·sizeof(*a))and covers the subscript (recallmemset_too_much). - Local copies. In a static function whose address is not taken, an
access through a local initialised from an unmodified parameter through
pointer conversions (
struct big *b = v;), neither of them ever modified or address-taken, is an access through the parameter; the requirement counts the bytes it reaches (sized(n), probe 09). An exported function’s copies give none: aqsortcomparator’sa->keystaysunresolved(unknown-extent). - Canonical loops. A loop index whose address is taken is not canonical (it may change through the pointer).
- Calls the unit makes to exported callees. A direct call in the unit to an exported or address-taken function gets the Call-site records of its requirements beyond Single (spatial only; the nullability stays the body’s). The link step still reports a violation at a cross-unit call through the callee’s summary (§13.2 step 4); deciding exported requirements at cross-unit callers for the program ledger (proven discharges, unresolved under A1) is left to S8.
- The null part at a call. It is enforced unguarded when some non-null
requirement is unguarded; otherwise under the first requirement’s guard,
and only the accesses covered by requirements with that guard are
null-proven in the body. A guarded
nonnullis the zero-length formnonnull_n(arg, g)with the guard termgcomputed as a have, which saturates at zero when the guard fails (§10.2), sof(NULL, 0)passes. - Guards with no exact term.
c < eis the terme - candc <= ethe terme - c + 1, folded into one term over the argument. A have stops a negative signed argument at zero before it adds (§10.2), so0 <= lastwould read as 1 forlast == -1: when the folded offset is positive and the argument is signed, the guard has no term and the requirement isunresolved(inexpressible)at that call (a pointer guard, R3’s, never has one). - Messages. A definite violation at the call keeps RFC 0011’s wording,
'<f>' requires <n> bytes behind '<a>', which has <m> bytes. - Retired may-requirements. A summary’s
requires-extent(a may-fact) is no longer reported at a call to a callee of the unit; declared kinds and §7.5 requirements report instead. Library rows and, at link, callees known only by another unit’s summary still report it (§13.2 step 4). - Ownership contracts.
mallocand theownership_*attributes outside system headers are contracts of the §5.1 kind: a fresh result of the family (malloc: the heap’s),ownership_takesreleases the argument andownership_holdsretains it.
7.6 Counted-field invariants (the designated first cut)
Section titled “7.6 Counted-field invariants (the designated first cut)”For struct types defined in the TU or in user headers, KindInference
runs Houdini over candidate invariants.
- Candidates. For a pointer field
dand an integer fieldfof the same struct, the candidates arecount(d) == f + candbytes(d) == f + c, withc ∈ {0, 1}. - Disqualification. Before round 1, every candidate of a struct is
dropped when any of these holds anywhere in the unit: the address of
dorfis taken; an object of the struct is written byte-wise (aLibrarySpecargument withworrwaccess that can reach it, or a character-typed store into it); or an object of the struct is copied or converted from an object of another type. These are the writes the store-group rule cannot see. - Round 1 is the unit’s authoritative engine run (§2.6), with every
candidate assumed at function entry (
EngineInput::fieldAssumptions, §14). Every creation site (allocation of the struct, declaration, compound literal) and every store group (§7.4 rule 7) touchingdorfmust re-establish each candidate by the engine’s facts at the end of the group. A zeroing creation (calloc, a zero-initialised declaration, a lowered allocation, §11) createsdnull andfzero; a nulldsatisfies a candidate only whenf + cis zero, soc = 1fails there. The engine publishes onestoreVerdictper candidate and group. Candidates that are not re-established are dropped. - Further rounds re-analyse only the functions containing a store group of a struct whose candidate was dropped, with the reduced set, and publish into a discarding adapter. They repeat until nothing is dropped, for at most three rounds in total. Candidates still standing after the third round are dropped.
- The authoritative rows. Round 1’s rows stand for every function that
loads or stores no field of a struct with a dropped candidate. Every
function that does is analysed once more after the last round, with only
the surviving invariants, and that run replaces its rows
(
LedgerAdapter::beginFunction). No published decision rests on a dropped candidate. - Application. A spatial facet that the engine left
unresolved(unknown-extent)may have a witness showing a load fromo->d, whered’s struct has a surviving invariant.LedgerAdapter::finishupgrades such a facet to checked againsto->f + cwhen that term is expressible (§10.3), and tounresolved(inexpressible)otherwise.
Only exact equalities survive, so a check against the invariant is never
tighter than the allocation. A surviving invariant becomes the field’s
inferred Counted or Sized kind, and it is an exact extent (§7.1). For
a struct defined in a header, the unit record carries the invariant, the
per-TU verdict (holds, violated or unknown) and whether the unit
relied on it. The link step verifies these (§13; A3).
Departure: the resolutions describe templates extent(p) == k*f + c.
The candidates are narrowed to k = 1 with c ∈ {0, 1} in element or
byte units. Those two units give the scales an allocation such as
malloc(f * sizeof *d) or malloc(f) produces, and each extra template
multiplies the store verdicts the engine must publish; wider templates are
left to RFC 0031.
This subsection is the designated first cut. If S6 overruns, it is removed and fields fall back to §7.3. Gate G13 is sized to pass without it.
Amendment (S6). Cut: S6 ships without the engine rounds.
KindInference still proposes the candidates and their disqualifications
(weavec --dump-kinds), but EngineInput::fieldAssumptions is empty, no
store verdict is published, and no field is exact by an invariant; the
RFC 0012 sized-field inference keeps its declared extent class (B2’s D14).
Amendment (S7). A narrower instance — an integer field lower-bound invariant, proposed and refuted by the same Houdini — was measured against G10’s residual family and is not built either. It would have bought nothing there, and it would not have survived its own store checks; G10 records the measurement and what the family actually is.
8. The library table
Section titled “8. The library table”One declarative table, lib/Core/LibrarySpec.txt, replaces three
scattered library models:
lib/Analysis/Builtins.cpp(529 entries);lib/Analysis/RuntimeModels.{h,cpp};- about 130
name == "…"tests for 54 libc functions in the transfer code.
CMake embeds the text in libweavec-core as a string. core::LibrarySpec
parses it once on first use; a parse error is a build-time test failure.
The engine and CheckPlanner both consult it. Gate H2 requires that no
libc name comparison remain outside it.
Which calls a row governs. A row applies to a direct call when both of these hold:
- the callee’s name is the row’s name, or one of its aliases: every row
Xalso answers to__builtin_X, and achk(…)clause maps the fortified forms (__builtin___X_chk,__X_chk) onto the row with their argument positions remapped. On macOS at-O1and above the SDK turnsmemcpy(d, s, n)into__builtin___memcpy_chk(d, s, n, __builtin_object_size(d, 0))andsprintf(d, f, …)into__builtin___sprintf_chk(d, 0, <size>, f, …), so without aliases every fortified call would miss the table; - neither the TU nor, at link, the program defines a function of that
name. A program’s own
strlcpy,getlineormallocuses its own body and summary, and zero-initialisation never redirects a call to it (§11).
A row applies to an indirect call whose solved slot contains the row’s
function (fp = memcpy; fp(d, s, n)): exactly, when the slot is closed
with that one target; otherwise the row’s spatial requirements are
unresolved(callback).
8.1 Entry schema
Section titled “8.1 Entry schema”namespace weavec::core {struct LibraryParam { enum class Type : std::uint8_t { Int, Pointer, Function, Other } type; enum class Access : std::uint8_t { None, Read, Write, ReadWrite } access; // bytes behind the pointer std::optional<LibTerm> bytes; // required accessible bytes from the pointer bool string = false; // must be NUL-terminated within its object enum class Null : std::uint8_t { Forbidden, Allowed, AllowedIfZero } null; std::optional<LibTerm> zeroTerm; // AllowedIfZero: null is allowed when this term is 0 enum class Effect : std::uint8_t { Borrow, Release, Realloc, Retain, Escape } effect; std::string family; // Release/Realloc: the release family std::string state; // Retain: the hidden state slot std::optional<LibCallback> callback; // Function parameters};struct LibraryResult { enum class Kind : std::uint8_t { Void, Int, Fresh, Static, Arg, Interior, InteriorState, Unknown } kind; std::string family; // Fresh std::string state; // Static, InteriorState: the hidden state slot it points into std::uint8_t arg = 0; // Arg / Interior std::optional<LibTerm> extent; // Fresh/Static: accessible bytes enum class Null : std::uint8_t { Never, OnFailure, May } null; bool zeroInit = false; // eligible for zero-init lowering (§11)};struct LibraryChk { // a fortified alias (§8, "Which calls a row governs") std::string name; // __builtin___memcpy_chk std::vector<std::int8_t> argumentOf; // for each alias argument, the row's argument index, or -1 (dropped)};struct LibraryEntry { std::string name, header; std::vector<LibraryChk> chk; std::vector<LibraryParam> params; bool variadic = false; LibraryResult result; std::vector<LibDisjoint> disjoint; // {a, b, length term} std::vector<std::string> invalidates; // state slots whose borrows end (setenv → "environ") std::vector<std::string> reads; // state slots read (strtok(NULL, …) → "strtok") bool noreturn = false, exits = false, returnsTwice = false; std::optional<LibFormat> format; // printf/scanf family: format argument index, first vararg};}LibTerm is a small expression over arguments:
term ::= INT | 'a' N | 'strlen(a' N ')' | 'fmtlen(a' N ')' | term '*' term | term '+' term | term '-' INT | 'min(' term ',' term ')'fmtlen(aN) is the length of the output snprintf would produce for the
format at argument N and the arguments after it. It states a requirement;
it is never evaluated as a check term. CheckPlanner enforces a fmtlen
requirement by the snprintf lowering of §10.4, or proves it from a static
maximum for a literal format whose conversions are all bounded (%d,
%c, %1.15g, a %s with a precision).
8.2 Text syntax
Section titled “8.2 Text syntax”One entry per line, with # comments and \ continuation. A pointer
parameter is access[:size][:flags]. A pointer parameter without bytes
or str requires a Single value (one element of its pointee type), and a
fn parameter without a callback clause is only stored or compared.
directive ::= 'header' NAME ';' | 'header' DIR '/*' ';' | 'builtins' ';'entry ::= NAME '(' params ')' '->' result { clause } ';'params ::= [ param { ',' param } ] [ ',' '...' ]param ::= 'int' | 'other' | 'fn' { ':' fflag } | access { ':' pflag }fflag ::= callback | 'null-ok'access ::= 'r' | 'w' | 'rw' | 'none'pflag ::= 'bytes(' term ')' | 'count(' term ')' | 'str' | 'null-ok' | 'null-if-zero(' term ')' | 'release(' FAMILY ')' | 'realloc(' FAMILY ')' | 'retain(' STATE ')' | 'escape' | 'init(' FAMILY ')' | 'fini(' FAMILY ')' | 'out(' value ')' | callbackcallback ::= 'sync(' [ INT { ',' INT } ] ')' | 'entry(' [ INT ] ')' | 'at-exit'result ::= 'void' | 'int' [ ':value(' term ')' ] | 'noreturn' | valuevalue ::= rkind { ':' rflag }rkind ::= 'fresh(' FAMILY ')' | 'static(' STATE ')' | 'arg(' INT ')' | 'interior(' INT ')' | 'interior-state(' STATE ')' | 'ptr'rflag ::= 'extent(' term ')' | 'null-on-failure' | 'nonnull' | 'null-ok' | 'zero-init' | 'no-zero-init' | 'str' | 'replaces' | 'or-fresh(' FAMILY ')' | 'or-static(' STATE ')' | 'offset(' term ')' | 'zero-filled'clause ::= 'disjoint(' INT ',' INT ',' term ')' | 'copies(' INT ',' INT ',' term ')' | 'fills(' INT ',' term ',' term ')' | 'writes-str(' INT [ ',' term ] ')' | 'invalidates(' STATE ')' | 'reads(' STATE ')' | 'exits' | 'returns-twice' | 'printf(' INT ',' INT ')' | 'scanf(' INT ',' INT ')' | 'chk(' NAME ':' INT { ',' INT } ')'term ::= INT | 'a' N | 'strlen(a' N ')' | 'fmtlen(a' N ')' | MACRO | term '*' term | term '+' term | term '-' INT | 'min(' term ',' term ')' | '(' term ')'Amendment (S4 preparation). Writing the full table showed that the
first grammar could not express several effects that the name == sites
of §8 encode, so these constructs were added; the header comment of
LibrarySpec.txt documents each one and is kept in step with this list:
header NAME;,header DIR/*;andbuiltins;directives. They define the §5.2 header list and set each following entry’s header.count(t), a requirement in elements rather than bytes (the wide functions), andMACROterms such asBUFSIZandPATH_MAX, evaluated in the calling unit. The never-defined__WEAVEC_UNBOUNDEDmarks a requirement that is never proven or checked (gets).out(v): the argument points to a pointer slot into which the call storesv(asprintf,strtol’s end pointer);replacesconsumes the slot’s previous value first (getline).init(F)andfini(F): the object behind the argument acquires or ends a resource of familyFwhile its own storage stays valid (regcomp,regfree,glob,globfree), sofinion a local is not an invalid release.arg(N):or-fresh(F)andarg(N):or-static(S): when argumentNis null the result is fresh (realpath(p, NULL),getcwd(NULL, n)) or static (tmpnam(NULL)).- String and copy facts that the engine’s string and memory transfer used
to hard-code:
copies(d,s,t),fills(d,v,t),writes-str(d[,t]),int:value(t)(strlenreturnsstrlen(a0)),offset(t)on interior results (stpcpy),stron string results andzero-filledoncalloc. - Overloads chosen by signature (glibc and BSD
qsort_r, GNU and XSIstrerror_r),fn:null-ok(signal(SIG_DFL)), an emptysync()for a callback that receives only the library’s own storage (nftw), and callback flags on pointer parameters for function pointers stored in the pointee (sigaction’sact).
The C++ schema in include/weavec/Core/LibrarySpec.h extends §8.1 with the
corresponding fields and is authoritative for them.
entry(N) means that argument N of the call is passed to the target;
entry() means the target receives no pointer from the call (signal).
chk(name: i0, i1, …) lists, for each argument of the fortified alias, the
row argument it becomes, with -1 for the extra size and flag arguments.
interior-state(S) is a pointer into hidden state slot S. Every
fresh(free) row carries zero-init or no-zero-init (§11).
Representative rows (the full table has about 600):
malloc (int) -> fresh(free):extent(a0):null-on-failure:zero-init;calloc (int, int) -> fresh(free):extent(a0*a1):null-on-failure:zero-init;realloc (rw:null-ok:realloc(free), int) -> fresh(free):extent(a1):null-on-failure:zero-init;reallocarray (rw:null-ok:realloc(free), int, int) -> fresh(free):extent(a1*a2):null-on-failure:zero-init;aligned_alloc (int, int) -> fresh(free):extent(a1):null-on-failure:zero-init;strdup (r:str) -> fresh(free):extent(strlen(a0)+1):null-on-failure:zero-init;free (none:null-ok:release(free)) -> void;memcpy (w:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) -> arg(0) disjoint(0,1,a2) chk(__builtin___memcpy_chk: 0,1,2,-1) chk(__memcpy_chk: 0,1,2,-1);memmove (w:bytes(a2):null-if-zero(a2), r:bytes(a2):null-if-zero(a2), int) -> arg(0);memset (w:bytes(a2):null-if-zero(a2), int, int) -> arg(0);strcpy (w:bytes(strlen(a1)+1), r:str) -> arg(0) disjoint(0,1,strlen(a1)+1) chk(__builtin___strcpy_chk: 0,1,-1);strncpy (w:bytes(a2):null-if-zero(a2), r:bytes(min(a2,strlen(a1)+1)):null-if-zero(a2), int) -> arg(0);strlen (r:str) -> int;sprintf (w:bytes(fmtlen(a1)+1), r:str, ...) -> int printf(1,2) chk(__builtin___sprintf_chk: 0,-1,-1,1,2);snprintf (w:bytes(a1):null-if-zero(a1), int, r:str, ...) -> int printf(2,3);write (int, r:bytes(a2):null-if-zero(a2), int) -> int;read (int, w:bytes(a2):null-if-zero(a2), int) -> int;system (r:str:null-ok) -> int;getenv (r:str) -> static(environ):null-ok reads(environ);setenv (r:str, r:str, int) -> int invalidates(environ);strtok (rw:str:null-ok:retain(strtok), r:str) -> interior-state(strtok):null-ok reads(strtok);qsort (rw:bytes(a1*a2), int, int, fn:sync(0,0)) -> void;pthread_create (w, r:null-ok, fn:entry(3), none:null-ok:escape) -> int;signal (int, fn:entry()) -> ptr;atexit (fn:at-exit) -> int;fopen (r:str, r:str) -> fresh(fclose):null-on-failure;fclose (rw:release(fclose)) -> int;freeifaddrs (none:release(freeifaddrs)) -> void;exit (int) -> noreturn exits;longjmp (other, int) -> noreturn;setjmp (other) -> int returns-twice;alloca (int) -> fresh(stack):extent(a0):nonnull:zero-init;Semantics:
fresh(F)creates an owned resource of familyF.stackis the frame’s family: it is released at return, and returning it islifetime-too-short(probe 23).static(S)returns a borrow of hidden state slotS, a global place named<S>.retain(S)stores the argument in slotSfor laterreads(S)calls. Releasing a retained value while it is retained makes the nextreads(S)a use-after-free (probe 25).invalidates(S)ends every borrow ofSwith aReleasedrecord. It isconditional(possible), because C does not guarantee that the storage moves (probe 25c).realloc(F)releases the argument on the non-null result class. On the null class it keeps the argument when the size argument is non-zero, and releases it when the size is zero, because glibc’srealloc(p, 0)freespand returns null (C23 makes a zero size undefined). The cases use the §9.1 grammar:outcome null a0 freed when param 1 =0. With a size known to be zero,q = realloc(p, 0); if (!q) free(p);is a definite double free; with an unknown size the record is conditional and the same code is a possible one. In the enforcing modes the zero-init wrapper maps a zero size to one (§11), which makes the behaviour the same on every C library.interior-state(S)returns a pointer into the valueSretains:strtok(NULL, …)points into the string an earlier call retained, not into its own argument.- Facets whose proof uses a
static(S)orinterior-state(S)extent, aretain/reads/invalidatesrelation or a callback clause aretrusted(library-spec).
String literals have a writable extent of zero. A write through a pointer
to one, directly or by a library argument with w access, is a definite
out-of-bounds (probe 25b).
With a literal format, a printf/scanf family entry checks that the
format’s conversions match the number of arguments. Too few is a definite
out-of-bounds, because the call reads past the variadic arguments. A
non-literal format makes the call’s spatial facet
unresolved(inexpressible).
8.3 Model fixes carried by the table
Section titled “8.3 Model fixes carried by the table”These rows fix the model:
system(NULL)is allowed.- Zero-length
read,write,memcpy,memmove,memset,memcmp,snprintfandstrncpyaccept null. Their null facets use the zero-length form of thenonnullcheck (§10.2), which traps only when the length is non-zero, so an empty vector’smemcpy(d->data, s->data, 0)with null pointers runs. getenv,strtok,localtime,strerror,getpwnamand similar functions return static storage with the right lifetime.longjmpisnoreturn.realloc(p, 0)may releasepon the null class (§8.2).freeifaddrs,freeaddrinfo,globfreeandregfreeare releasers.
Clang 23 does not turn a declaration’s nonnull attribute (glibc’s
__nonnull on memcpy) into an IR nonnull argument, and LLVM does not
treat a llvm.memcpy with a non-constant length as proof of non-null; both
were checked against the reference toolchain. So a zero-length null
argument does not let the optimiser fold away a later WeaveC null check. A
rewrite-oracle case (memcpy(NULL, x, 0) followed by a checked
dereference that must trap) keeps it that way across LLVM upgrades.
Model changes made by converting Builtins, RuntimeModels and the name ==
sites are listed in the S4 ledger diff (§Implementation plan). Every row
has a unit test in unittests/Core/LibrarySpecTest.cpp. The expectations
come from an independent hand-written table (effect, nullability, extent
and clauses per function), not from the row itself, so a wrong row fails
its test. The macOS and glibc fortified spellings each have a test.
8.4 Allocation failures and leaks
Section titled “8.4 Allocation failures and leaks”The new allocation-failure id is off by default. It reports a use of an
allocation result that is null or maybe-null from allocatorSource
without a test. The use is checked either way, so an out-of-memory path
traps instead of corrupting memory. This covers the only true positives
the corpus produced (7 unchecked malloc results).
leak is always a warning. It is not reported for resources live at a
return from main, or at a call whose entry has the exits clause
(exit, _Exit, quick_exit, abort).
9. Temporal precision
Section titled “9. Temporal precision”9.1 Outcome-keyed effects
Section titled “9.1 Outcome-keyed effects”FunctionSummary::outcomes (RFC 0006) already keys consumption by the
result’s class, and RFC 0009 guards can condition it on arguments. The
dump for Lua’s l_alloc is:
outcome null ptr freed when nsize =0outcome nonnull ptr movedWhat is missing is the derivation for the common wrapper shape, where the
free happens on a path that returns a local (repros realloc2–realloc4,
jansson’s jsonp_realloc):
void *my_realloc(void *ptr, size_t old, size_t n) { void *m = malloc(n); if (m && ptr) { memcpy(m, ptr, old < n ? old : n); free(ptr); } return m;}The free’s MoveRecord carries the guard m nonnull ∧ ptr nonnull.
Today, deriving the summary drops conjuncts on locals, so the summary says
ptr: freed whatever the result.
Derivation rule. At each return e where e names a local place r,
every conjunct on r in the guard of a consume record is translated into
the result’s class set before the summary is derived:
r nonnullbecomes classnonnull;r nullbecomes classnull;r = 0becomes classzero;r positive|negativebecomes the matching integer classes.
The record’s contribution goes to outcomes[class] instead of the
unconditional effects. Conjuncts on parameters stay as guards. All
other conjuncts are dropped, which keeps the effect on more classes.
Case keys. After derivation, each consume effect’s key is canonicalised to the grammar below:
case ::= 'result' class-set [ 'and' ptest ]class-set ::= class { '|' class } (class as in SummaryIO: null nonnull zero positive negative)ptest ::= 'param' INT ( '=0' | '!=0' )A guard conjunct that is not a single parameter zero-test is dropped, and the effect then holds on more paths. At most two distinct cases with a non-empty consume set are kept per function. If there are more, all classes are joined into the unconditional may-effect.
Lossy effects. Dropping a conjunct makes an effect hold on paths where
the program does not perform it. That is safe for proofs but not for
definite errors: a wrapper that frees ptr only when m nonnull ∧ flag,
with flag a local, would otherwise yield a definite use-after-free in a
caller on paths where flag was false. So every effect produced by
dropping a conjunct, or by folding classes into the may-effect under the
case limit, carries a lossy bit. A record made from a lossy effect stays
conditional even after a result test selects its class (§3.1), so it can
only ever give a possible warning.
SummaryIO format 27 spells cases with the existing
outcome <class> <path> <flags> [when …] lines, restricted to this
grammar, and spells the bit as the flag lossy.
Amendment (S7). Three corrections from the implementation.
- The effect’s
whenkeeps every conjunct the summary can name, not one parameter zero-test. The kept conjuncts are exactly the onessummaryGuardOfcan project onto caller-visible paths, which includes the integer predicates a library row’s zero-size release rests on (realloc(p, n)frees on its null class only whennis zero, §8.2); dropping them would have made every wrapper of such a row claim the release unconditionally. Keeping more conjuncts is sound — the callee performs the consume only when all of them hold — and strictly more precise, so only the case key is restricted to the grammar above: the classes that consume the same paths under the same conditions form one case, and more than two cases with a non-empty consume set fold into the unconditional may-effect withlossyset. - A conjunct the facts at the
returnalready decide is not a condition on that return, so dropping it widens nothing and does not make the effect lossy. Without this,int grow(struct vec *v) { … if (!bigger) return 0; … return 1; }would mark its move ofv->itemslossy on the class thebiggertest selects, and every caller would lose a definite use-after-move that RFC 0008 reports today. The guard is pruned against the state at the return before what is left is tested for exportability. - The guard a case is keyed by is the guard over places the consume
happened under, which the summary’s
whenhas already lost. It is kept beside the flow-sensitive consume event, joined exactly as the event is, so that a second, unguarded consume of the same path leaves nothing to key on.
The format version is 29: 27 as above, 28 for unknown (§5.1) and 29 for
lossy.
At a call site, the cases are applied through the existing
PendingOutcome machinery:
- A consume selected by a test of the result, or by a known argument value
satisfying
ptest, becomes unconditional;conditionalis cleared (§3.1) unless the effect islossy. - Without a test, it is a
conditionalrecord, which gives a possible warning.
Repros realloc0–realloc5 and luaalloc become exact-expectation cases.
9.2 Non-null facts from guard functions
Section titled “9.2 Non-null facts from guard functions”This rule fixes repro pred.c and the cJSON_Is* cluster. For parameter
i, let K be the may-set of result classes over every path on which
the parameter is not proven non-null at the return: paths where it is
Null or MaybeNull, and paths where its nullness is unknown because it
was never tested. Then for every class c ∉ K, nonNullOn[c] gains
param i: a result in class c implies that the parameter was non-null.
The rule is the contrapositive, and it is applied when the summary is
finalised. It is derived only from the default-context analysis within
budget, never from a context run or an incomplete summary.
if (!cJSON_IsString(x)) return; x->valuestring then proves x non-null
on the fall-through edge. For int ok(struct x *p, int k) { if (k) return 1; if (!p) return 0; return 1; }, the k path returns 1 without testing
p, so K contains both classes and no fact is derived: after
if (!ok(p, 1)) return;, p->v keeps a checked null facet. This is a case
under test/cases/semantics/.
9.3 Function-pointer slots
Section titled “9.3 Function-pointer slots”SlotCollector builds flow-insensitive, field-based constraints for every
function-pointer value in the TU. Slot keys:
field <record type key> <field>(array elements share the field);global <name>(a TU-local static becomesstatic <unit>:<name>);param <function> <i>andresult <function>;local <function> <name>(TU-private, eliminated before export).
Constraints:
f ∈ S: a function designator stored, assigned, passed or returned intoS, including in static initialisers.S ⊆ T: a copy of a value from slotSintoT. Direct calls add argument-to-parameter and result-to-receiver copies. Casts between function-pointer types preserve the value.open(S):Sreceives a value from outside the solved program. Sources are a parameter of an exported function, the result of an unknown callee, a load from memory the analysis cannot name, or an integer conversion.- Dynamic call constraints: for an indirect call through
S, and each targetf ∈ S, the arguments flow intoparam f <i>andresult fflows to the receiver.
The solver (core::FnSlots) iterates set inclusion to a fixpoint over
function names; it is linear in the number of constraints times the number
of targets. Each unit’s record exports its constraints, with local slots
eliminated. The link step and weavec --whole-program solve them over all
records. This replaces the RFC 0014 callback-global fixpoint and its
callbackGlobals export, which are deleted in S7; their lit and
evaluation cases are kept as cases and must still pass.
Closed slots. A slot is closed when every value it can hold is visible to the solver.
- In a per-TU compile, only two kinds of slot can be closed: fields of
struct types defined in the main file (not in a header) whose objects
never flow through parameters, results, unknown callees or externally
visible globals, and
staticglobals whose address does not escape. Every other slot is open, because objects of a header-declared struct can come from other units with other targets. - At link, when the output is an executable, a slot is closed when every
unit that can store into it has a record. A parameter of an exported
function is closed only when no
unanalyzed-inputexists, the function’s address never reaches an open position, and none of-shared,-r,-rdynamicor-Wl,-export-dynamicis given.
At an indirect call through S:
Sclosed, one target: the call is analysed exactly as a direct call.Sclosed, several targets: the join of their summaries. A may-effect from any target is a may-effect. A consume is unconditional only if every target consumes unconditionally.Sopen, with known targets: as closed for temporal facts only. The call’s temporal facet istrusted(extern-contract), and the detail names the open source (for example “values stored byjson_set_alloc_funcsparameter 1”). Spatial and null facts of the result never come from an open slot: the result gets the default of §7.3 for functions outside the TU (Single-or-nullable, null checked). Spatial and null outcomes are decided per TU and copied verbatim at link (§1), so they may not rest on a per-TU view of an open slot.Sopen, without known targets: the unknown-callee default with reasoncallback(§5.1).
Every indirect call also has a null facet on its callee operand, checked
with the function-pointer form of nonnull (§10.2) unless proven.
Lua’s g->frealloc is stored from lua_newstate’s parameter, and
luaL_newstate passes l_alloc. So in whole-program mode the slot is
{l_alloc}, and the two injected allocator bugs are hard gate G12. In a
per-TU compile of lstate.c the slot is open and empty: the calls are
unresolved(callback), not silent.
Amendment (S7). What the function has watched the callee operand hold
wins over the slot: g = unknown; g(p); calls the value just stored, and
the flow-insensitive slot, which also holds every other value stored into
g anywhere, does not speak. The slot decides an indirect call only where
the flow-sensitive targets say nothing. The solution the engine resolves
through is the program’s when the link step or --whole-program has solved
one (it reaches the engine on the program database) and the unit’s own
otherwise; the §7.3 slot kinds stay the unit’s either way, because
spatial and null outcomes are decided per TU and copied verbatim at link
(§1).
Two more implementation rules. A slot with more targets than a
CallTargets can name (MaxCallTargets, 32) says little and costs a
summary join and a context run per target at every call through it, so it
takes the §5.1 default instead; that bound is what CallTargets::join
already applies to the flow-sensitive targets.
An indirect call whose slot has a single target that a LibrarySpec row
governs gets that row’s argument sites, as a direct call to the target
would: fp = memcpy; fp(dst, src, n) is checked against memcpy’s row. When
the slot is open, the row still decides only the temporal facts, so those
needs are unresolved(callback). Only what the row needs follows the
slot: what it allocates or releases decides ownership and §11’s
zero-initialisation, which are the syntactic call’s business (gate G8’s
oracle takes an allocator’s address and expects its wrapper), so a row that
allocates or releases gives the indirect call no sites of its own.
9.4 Boundary invariants
Section titled “9.4 Boundary invariants”BoundaryInvariants checks two invariants at every boundary. The
boundaries are every Call site (boundary: call), every LibCall site whose
entry has a sync, entry or at-exit callback clause (boundary: call),
and every function exit (boundary: exit), including calls that do not
return (exit, _Exit, abort, longjmp, siglongjmp, and any other
exits or noreturn entry or declaration), because atexit handlers,
signal handlers and setjmp sites run after them. It works over
the places reachable from parameters and globals, which the engine
publishes as BoundaryFacts (§14):
- Validity (
dangling-escape). A reachable place that may hold a released pointer, or a pointer to storage whose lifetime has ended (automatic storage, a compound literal,allocastorage), and that is not overwritten before the boundary, gives the boundaryunresolved(dangling-escape). - Owner uniqueness (
second-owner). A slot is owning if some function in the TU releases a value loaded from it, which is a flow-insensitive fact. Two distinct reachable owning places that may hold the same owned object give the boundaryunresolved(second-owner).
Both invariants are assumed at function entry (A1 and A3). That is what
makes free(s->a); free(s->b) proven inside cleanup, and proven
temporal facets of loads from parameters sound. They never produce an
error except under require levels.
Propagation. An assumption that a boundary elsewhere breaks cannot
prove anything. Each boundary row names the place class it concerns: a
global g, or a field path <struct>.<field>… for places reached through
parameters. LedgerAdapter::finish then downgrades, in the whole unit,
every temporal facet that relied on the entry assumption for a place of
that class to the boundary’s reason. The link step does the same over the
program ledger. In probe 02, remember(p); free(p); return peek(); leaves
the global g_cache dangling at the call to peek; the propagation makes
g_cache[0] inside peek, the ASan-reported bug site,
unresolved(dangling-escape) instead of proven.
For probes c10 and c10c, the ordinary rules still report double-free
at the call. The boundary adds unresolved(second-owner), so neither
program is ever proven.
Amendment (S7). Four points the implementation settles.
- What each boundary sees. At a Call boundary (and at a call that does
not return, whose continuation is the
atexit, signal andsetjmphandlers) the reachable places are the objects the arguments point to, plus the globals: that is what the other side assumes A1 and A3 about, and it is never told otherwise. At the exit that returns to the caller, a release before the return is already carried to the caller by the summary, which applies it; only storage whose lifetime has ended is reported there. Reporting the released places again at that exit would make every cleanup function break the invariant for the field it frees, which would defeat the very proof the invariant exists to give (free(s->a); free(s->b)insidecleanup). - The result counts. The place a
returnhands back is covered by the exit boundary, though it is neither a parameter nor a global: what the caller does with the value rests on the same invariant. This is what makes probe 27’s bug site non-proven, as the S3-exit amendment expects. - Which facets the propagation reaches. A temporal facet “relied on the
entry assumption for a place of that class” exactly when it is proven
at a site whose operand — the pointer the site’s facets are about — is
loaded from a place of that class. Only a proven facet is downgraded: a
checked one is enforced at run time and a trusted one rests on its own
reason, so neither rests on the assumption, and no planned check is
lost. A broken field class
struct s.bufalso breaks the class of the record that holds it,struct s, because a load of the field goes through the object; that is what carries probe 02d’s row fromdrop’s release to the load insidepeek. - Owner uniqueness inside a function. The flow-insensitive notion is
applied at each consume: a name reached through an interior alias edge
holds a pointer into the released object rather than the object, so it
is not an owning place. Its record is kept, so a use through it is still
reported where it arises (§3.1, Aliases of a released object), but the
record is
local: no summary says the caller’s value there was released, and the caller gets theunknowneffect (§5.1) for it instead. Without this, releasing one buffer marked every pointer into it — a stack pointer, a cursor, acontainer_ofparent — as a released owner, and two callees in a row each reporting that read as a double free.
10. Check planning and emission
Section titled “10. Check planning and emission”10.1 CheckPlan
Section titled “10.1 CheckPlan”CheckPlanner turns each checked requirement record (§2.5) into a
core::CheckPlan entry, whatever its facet’s merged outcome, plus a guard
for each lowered violation (§3.4). CheckPlanner::plan is pure: it runs
inside LedgerAdapter::finish in every mode, including weavec and
-fweavec-checks=none, and decides expressibility (§10.3) before the
require-level errors and the summary line. A record whose terms are not
expressible becomes unresolved(inexpressible) there, so the ledger is the
same whether or not checks are emitted.
struct CheckTerm; // constant | place handle (a decl plus a field/deref path) | sizeof // | div (floor division by a positive constant; extents only, amendment S3) // | + - * (evaluated by the term helpers) | strnlen(term, term)struct CheckPlanEntry { SiteId site; Facet facet; std::uint16_t requirement; // index into the row's requirements enum class Template : std::uint8_t { Nonnull, Index, Span, Len, Disjoint, Assert } kind; enum class Form : std::uint8_t { Plain, IfNonZero, Function, Result, Violation } form; enum class Placement : std::uint8_t { WrapOperand, WrapIndex, WrapArgument, ReplaceAccess, BeforeCall, ReplaceCall } placement; std::uint8_t argument = 0; // WrapArgument std::vector<CheckTerm> operands; // the template's extra operands, in order std::optional<CheckTerm> guard; // §7.5 requirement guard: check only when it holds bool proven = false; // verify mode: a check of a proven facet};The six templates keep their names, and two have extra forms. nonnull
has a zero-length form (IfNonZero, for null-if-zero arguments) and a
function-pointer form (Function, for indirect callees). len has a
result form (Result) for the snprintf lowering of sprintf. The
Violation form, of any template, is the unconditional trap for a lowered
violation that has no expressible check of its own (§3.4).
Departure: the resolutions list six templates with fixed signatures.
The forms are needed to avoid false traps on zero-length null arguments,
to cover null callbacks, and to check sprintf without evaluating its
arguments twice; span also takes an index operand (§10.2) so that a
cursor subscript never forms an out-of-bounds pointer.
A place handle is resolved in Analysis to a clang::ValueDecl plus a
member path. Core never sees Clang.
10.2 Templates and the prelude
Section titled “10.2 Templates and the prelude”The helpers are static functions with always_inline, nodebug and
unused, appended to the predefines buffer in BeginSourceFileAction
under a pragma that silences -Weverything. The prelude uses no macros
(__SIZE_TYPE__ and friends are not expanded in preprocessed .i input),
spells sizes as __typeof__(sizeof 0) and 64-bit values as
unsigned long long/long long, and calls only builtins. The trap
reason is the template name, which is also what the TRAP markers of §17.3
match. Amendment (S2). Because the helpers are nodebug, the
debugger attributes the trap to the access’s own source line and column, but
the reason string that __builtin_verbose_trap places in debug info is lost
with the helper’s frame. The location is the more useful of the two, so
nodebug stays; report mode and the verify category still name the
template. In trap mode (abridged; A stands for
static __inline__ __attribute__((always_inline, nodebug, unused))):
A void *__weavec_chk_nonnull(const volatile void *p) { if (__builtin_expect(p == 0, 0)) __builtin_verbose_trap("weavec", "nonnull"); return (void *)p;}/* nonnull, zero-length form: null is allowed when n == 0 (§8.3). */A void *__weavec_chk_nonnull_n(const volatile void *p, unsigned long long n) { if (__builtin_expect(p == 0 && n != 0, 0)) __builtin_verbose_trap("weavec", "nonnull"); return (void *)p;}/* nonnull, function-pointer form: the callee operand of an indirect call. */A void (*__weavec_chk_nonnull_fn(void (*f)(void)))(void) { if (__builtin_expect(f == 0, 0)) __builtin_verbose_trap("weavec", "nonnull"); return f;}A unsigned long long __weavec_chk_index(unsigned long long i, unsigned long long n) { if (__builtin_expect(i >= n, 0)) __builtin_verbose_trap("weavec", "index"); return i;}/* The element p[i] of `width` bytes lies inside [base, base + bytes). * The address is computed in integers; p + i is never formed unchecked. */A void *__weavec_chk_span(const volatile void *p, long long i, const volatile void *base, unsigned long long bytes, unsigned long long width) { long long off = (long long)((unsigned long long)p - (unsigned long long)base), step, at; if (__builtin_expect(p == 0 || __builtin_mul_overflow(i, (long long)width, &step) || __builtin_add_overflow(off, step, &at) || at < 0 || bytes < width || (unsigned long long)at > bytes - width, 0)) __builtin_verbose_trap("weavec", "span"); return (char *)p + step;}A unsigned long long __weavec_chk_len(unsigned long long need, unsigned long long have) { if (__builtin_expect(need > have, 0)) __builtin_verbose_trap("weavec", "len"); return need;}/* len, result form: the snprintf lowering of sprintf (§10.4). */A int __weavec_chk_len_r(int written, unsigned long long have) { if (__builtin_expect(written >= 0 && (unsigned long long)written >= have, 0)) __builtin_verbose_trap("weavec", "len"); return written;}A void *__weavec_chk_disjoint(const volatile void *d, const volatile void *s, unsigned long long n) { unsigned long long a = (unsigned long long)d, b = (unsigned long long)s; if (__builtin_expect(n != 0 && (a < b ? b - a < n : a - b < n), 0)) __builtin_verbose_trap("weavec", "disjoint"); return (void *)d;}A void __weavec_chk_assert(int c) { if (__builtin_expect(!c, 0)) __builtin_verbose_trap("weavec", "assert");}/* Bounded string length for str requirements; traps on null. */A unsigned long long __weavec_strnlen(const char *s, unsigned long long max) { unsigned long long n = 0; if (__builtin_expect(s == 0, 0)) __builtin_verbose_trap("weavec", "nonnull"); while (n < max && s[n]) ++n; return n;}Term arithmetic. Extra terms are never computed with C’s own
arithmetic, which wraps for unsigned types, is undefined on signed
overflow, and turns a negative count into a huge unsigned one. +, - and
* in a term are calls to small term helpers over 64-bit integers, with
explicit signedness:
- a signed leaf enters as
long long; a negative value used as an extent or count is 0, so every access past a negative count field traps (struct b { char *WEAVEC_COUNTED_BY(len) d; int len; }withlen == -1); - on overflow, a helper computing a need (a length, an offset, a
requirement) saturates to the maximum, and a helper computing a have
(an extent) saturates to 0. Both fail closed, so a wrapping
nmemb * sizeforqsorttraps instead of passing; x - kin an extent saturates at 0.
A term that no longer fits in 64 bits after conversion, or a leaf of an
unsigned type narrower than size_t that the program itself computed with
wraparound, is taken as the value the program computed (an allocation’s
actual size argument, §7.4) or is inexpressible. The index operand of
index is converted to unsigned 64 bits, so a negative index fails
i >= n; the index of span stays signed, so p[-1] on a cursor inside
its object passes.
The helpers differ by mode:
- Report mode. The same names take three extra trailing arguments,
(const char *file, unsigned line, unsigned column). Instead of trapping they callvoid __weavec_rt_report(const char *template, const char *file, unsigned line, unsigned column)and return normally, which gives no guarantee (§Soundness). WithWEAVEC_RT_ABORT=1the runtime aborts instead. - Lowered violations. An unconditional
__builtin_verbose_trap("weavec", "violation")inline at the site (§3.4); in report mode, a report with templateviolation. - Verify mode. A second family,
__weavec_prv_*, is identical except for the trap category"weavec.proven". It is used for checks of proven facets. - Compilers without
__builtin_verbose_trap. The helpers use__builtin_trap().weavec-ccalways embeds a Clang that has it; the fallback exists forruntime/weavec_chk.c(§10.9), which the host compiler that builds WeaveC compiles.
10.3 Expressibility
Section titled “10.3 Expressibility”An operand that the rewrite wraps in place is evaluated exactly once and is always expressible: the pointer of a dereference, the index of a subscript, the argument of a call. That is why the null facet always has a check.
Every extra term is expressible only if all of the following hold:
- It is built from constants,
sizeof, parameters, non-volatile locals whose address is not taken,constlocals, and fields reached from those through.and->. - It contains no call except the prelude’s own helpers: the term
arithmetic helpers and
__weavec_strnlenover a side-effect-free pointer. Astrlen(aN)in a need term is computed as__weavec_strnlen(aN, have), which reads at mosthavebytes; the check fails the same way whenever the true length is at leasthave. Astrrequirement is checked as__weavec_strnlen(p, have) < haveagainst an exact extenthaveofp; without one it stays unresolved, or trusted when a caller contract covers it.fmtlenis never a term (§8.1). - It has no side effects:
Expr::HasSideEffects(ctx, true)is false, it has no volatile access, and it contains no assignment or++. - The engine proves that each place in it still holds, at the site, the value the extent was derived from. In other words, the engine’s extent term is expressed over places that are unmodified since the derivation. A term over a snapshot place with no C name fails this, as in the example in §4.
- Each of its own memory accesses is proven, trusted, or checked by the
term helper itself, at the site. Reading
b->capneedsbproven or trusted non-null and live;__weavec_strnlenchecks its own pointer for null and reads at mosthavebytes. Amendment (S6): a read through a pointer not proven non-null is checked by the term itself,((T *)__weavec_chk_nonnull(b))->cap(a place step marked checked), since the operands ofb->data[i]are unsequenced and the access’s ownnonnullwrap ofbmay run after the count is read. Amendment (S6): rule 4 also covers the count of a value loaded from a declared slot: when the pointer to its object is overwritten (d = b->data; b = other;), the extent keeps the value it had as an allocation-time snapshot, which no check can name, so the access isunresolved(inexpressible). - It fits in 64 bits.
- No argument of the call and no part of the wrapped operand may write a
place the term names.
memcpy(++p, q, n)checked against the remaining bytes of the oldp, orp[shrink(s)]checked againsts->len, would test the wrong value. - It compares against an exact extent or a declared kind, never a lower bound (§7.1).
A requirement with a failing term is unresolved(inexpressible), decided by
CheckPlanner::plan before any code is emitted.
10.4 Side effects and placement
Section titled “10.4 Side effects and placement”Rewrites never duplicate the evaluation of a user expression:
WrapOperand,WrapIndexandWrapArgumentreplace a subexpression with a helper call that returns it.ReplaceAccessreplaces a cursor accessp[i](or*p,p->f, with index 0) by*(T *)__weavec_chk_span(p, i, base, bytes, sizeof(T)), or the member access on that result, sopandiare each evaluated once and the address is formed only after the check.BeforeCallrewritescallinto(check, call)with the comma operator. The call’s type and value category are kept, and the check’s operands must be expressible extra terms. A §7.5 requirement guard wraps the check:(guard ? check : 0, call).ReplaceCallis used for Assume sites and for theprintffamily.sprintf(d, fmt, …)becomes__weavec_chk_len_r(snprintf(d, have, fmt, …), have), andvsprintfbecomes the same overvsnprintf. The write stays in bounds, the arguments are evaluated once, and the call returns the same value whenever it does not trap. It needssnprintf/vsnprintfdeclared in the unit (they are whenever<stdio.h>is included); otherwise, and for formats containing%n, the requirement isunresolved(inexpressible).
Sequencing between the original operands is unchanged. C leaves the order of function arguments unspecified, and the extra terms are side-effect free and name no place an argument writes (§10.3 rule 7), so evaluating a term in a helper does not change the result.
When one operand has several checks, nonnull is innermost, then
index. A span check subsumes nonnull (it traps on a null pointer), so
the planner never emits both on one operand.
Call-site checks of static-callee requirements (§7.5) and declared
requirements use WrapArgument for nonnull/span on the argument, and
BeforeCall with len for count requirements. null-if-zero arguments
use the zero-length form of nonnull, whose length operand must be an
expressible term.
Amendment (S5). Until the engine publishes witnesses for them, a
sprintf or vsprintf call defaults to checked by its lowering only when
the destination has an extent the declarations give, the bounded writer is
declared in the unit, and the format is a literal whose conversions neither
read a string argument (%s) nor write through one (%n): the lowering is
then the call’s whole spatial requirement. Its witness is a Length
witness without a need term, which CheckPlanner turns into the result
form. The prelude’s own helpers, in the predefines buffer, are neither sites
nor zero-initialised. CheckEmitter orders one site’s rewrites itself
(operand, index and argument wraps first, nonnull innermost, then the
replacements, then BeforeCall), since the plan’s order puts facets before
templates. A BeforeCall check at a return; becomes { check; return; },
and at the end of a body its last statement. Whether a violation was
lowered (§3.4) is answered per facet: when the -W flags lower the error of
any id that reports a violation of that facet. A violation whose error
stands fails the compile before anything is emitted, so the answer matters
only for the ledger’s check of a unit that fails anyway.
10.5 DeferredCodeGenConsumer
Section titled “10.5 DeferredCodeGenConsumer”WeaveCWrapperAction::CreateASTConsumer (lib/Frontend/Driver.cpp,
currently building a MultiplexConsumer{WeaveC, CodeGen}) returns a
DeferredCodeGenConsumer. That consumer owns the inner CodeGen consumer
and the WeaveC unit driver, and it is a clang::SemaConsumer.
It forwards these immediately:
Initialize,InitializeSema,ForgetSema;GetASTMutationListener,GetASTDeserializationListener;PrintStats,shouldSkipFunctionBody.
It records these, in call order, and forwards none of them yet:
HandleTopLevelDecl(returnstrue),HandleInlineFunctionDefinition,HandleInterestingDecl;HandleTagDeclDefinition,HandleTagDeclRequiredDefinition;CompleteTentativeDefinition,CompleteExternalDeclaration,HandleImplicitImportDecl;HandleOpenACCRoutineReference, which C reaches under-fopenacc;- for completeness, although C never calls them:
HandleTopLevelDeclInObjCContainer,HandleCXXImplicitFunctionInstantiation,HandleCXXStaticMemberVarInstantiation,AssignInheritanceModelandHandleVTable.
The consumer overrides every virtual of ASTConsumer and SemaConsumer in
LLVM 23, forwarding or recording each. The list is part of the
LLVM-upgrade checklist: a virtual added upstream and not overridden would
reach CodeGen out of order.
HandleTranslationUnit(ASTContext &) then:
- runs the unit analysis and planning (§1 step 2) and reports its diagnostics;
- if no error occurred and checks are on, runs
CheckEmitterover the plan; - replays the recorded calls in order;
- calls the inner
HandleTranslationUnit. CodeGen drops the module if any error occurred, as it does today.
Deferral is required. Without it, CodeGen has already emitted external
functions by the time the analysis runs; the prototype showed that only
lazily emitted static functions received their checks. The consumer is
used for every C code-generating action, whatever the checks mode. With
-fweavec-checks=none it changes nothing (gate G7).
Amendment (S2). “Changes nothing” holds for the corpus (gate G7: 153
TUs × 3 configurations byte-identical), not for every C input. Because the
replay sees the finished AST, CodeGen observes redeclarations that follow a
recorded callback. Three differences were found in hand-written units:
a C99 or GNU inline definition that a later extern declaration makes
external is emitted at a different position in the object; a tentative
array completed after its first use likewise; and weak_import,
availability or visibility on a later redeclaration now also applies to
earlier references (an extern_weak or hidden reference). The first two
change only symbol order. The third follows the declaration’s final
attributes, as a non-incremental compiler would; it is documented in
DeferredCodeGenConsumer.h, and matching the eager order exactly would need
CodeGen internals, which is left to RFC 0031 if it matters in practice. It is not installed
for -E, -fsyntax-only, dependency-only actions, or C++/Objective-C
inputs, which pass through to Clang unchanged.
10.6 CheckEmitter
Section titled “10.6 CheckEmitter”CheckEmitter builds every rewrite through Sema:
DeclRefExprto the helper;Sema::BuildCallExpr(nullptr, …);Sema::ImpCastExprToType(…, CK_BitCast)back to the operand’s pointer type, so a dereference of the result is still an lvalue;Sema::BuildBinOp(nullptr, …, BO_Comma, …)forBeforeCall.
Each rewrite runs inside a Sema::TentativeAnalysisScope, which
suppresses the diagnostics of provisional analysis, together with a
DiagnosticErrorTrap to observe whether an error occurred. A result is
rejected when isInvalid() or get()->containsErrors(), and
DiagnosticsEngine::hasErrorOccurred() must be unchanged afterwards; a
unit test forces an invalid rewrite and checks all three. Stores, compound
assignment, ++, &p[i], s->f and array-to-pointer decay all keep
working, as the prototype showed at -O0 and -O2, with -g and ASan,
and under -std=c89 -pedantic-errors -Weverything -Werror.
A rewritten child is put back through a parent-map child replacement on
the semantic form of the tree (the form CodeGen reads), not only through
the typed setters the prototype used (setRHS, setBase, setSubExpr).
Operands under an OpaqueValueExpr source were already excluded by the
planner (§2.1).
A rewrite that fails at this point cannot be turned into a ledger change,
because the ledger and the require-level errors were decided from the plan
(§10.1). The subtree is restored, never partially rewritten, and the
failure is reported as an internal error that fails the compile: “WeaveC internal error: could not insert the check for '<text>'; build with -fweavec-checks=none to bypass”. Failing closed keeps the guarantee; the
rewrite oracle (G8) and the corpus builds (G9, G11) make such a failure a
release blocker. The
-O2 pipeline removes checks the program already guards: a loop
for (i < n) p[i] checked against n keeps no trap.
10.7 Modes
Section titled “10.7 Modes”-fweavec-checks= |
Unproven facets | Proven facets | Zero-init default | Guarantee |
|---|---|---|---|---|
trap (default) |
__weavec_chk_*, trap |
nothing | on | yes |
report |
__weavec_chk_* report variant; links libweavec_rt.a |
nothing | on | only with WEAVEC_RT_ABORT=1 |
verify |
__weavec_chk_*, trap |
__weavec_prv_* where expressible (trap category weavec.proven) |
on | yes |
none |
nothing | nothing | off | no |
Verify mode is the soundness monitor. A weavec.proven trap means the
engine proved something false, and CI runs the probes, the cases and every
corpus test suite in this mode (gate G6). For verify mode the engine also
publishes a witness (§14) for proven spatial facets, not only for checked
ones, so that their extent and offset terms can be checked; the ledger
records which proven facets received a verify check
("check": {…, "proven": true}) and the summary counts them. Temporal
facets are never checked at runtime in any mode.
runtime/weavec_rt.c (installed as lib/weavec/libweavec_rt.a) defines
__weavec_rt_report. It prints
weavec: runtime check failed: <template> at <file>:<line>:<column> to
stderr once per site, where <template> is nonnull, index, span,
len, disjoint, assert or violation, and aborts instead if
WEAVEC_RT_ABORT=1 is set. weavec-cc adds it to the link line when the
link is given -fweavec-checks=report. Report mode is not cuttable: the
case runner and gate G11 use it to attribute traps to lines and templates
(§17.4).
10.8 Require levels at emission
Section titled “10.8 Require levels at emission”The errors of §6.3 are reported in step 3 of §1, after planning and before
emission, so they see the planned expressibility and a failing unit
produces no object. WEAVEC_REQUIRE_SAFE is read by AttributeReader and
applies per function.
10.9 Precompiled headers and modules
Section titled “10.9 Precompiled headers and modules”Injecting the prelude into predefines would change the predefines of a
PCH built by plain Clang. When -include-pch, -fmodules or an implicit
PCH is in use, CheckEmitter does not inject the prelude. It builds the
helper FunctionDecls directly in the AST as extern declarations without
bodies (building declarations through Sema needs no function scope), and
weavec-cc links lib/weavec/libweavec_chk.a, built from
runtime/weavec_chk.c, which defines the same helpers out of line. The
checks then cost a call; only PCH and module builds pay it, and only they
need the library. Building helper bodies in the AST is left to a later RFC:
it needs function-scope set-up in a release Clang with assertions off,
which the prototype never exercised. Preprocessor-only jobs, dependency
files and ccache -E never see the prelude, because only code-emitting
actions inject it. weavec-cc -fweavec-print-prelude prints the prelude
for the current mode, which the rewrite-oracle tests use (G8).
11. Zero-initialisation
Section titled “11. Zero-initialisation”In the enforcing modes (checks other than none, unless
-fno-weavec-zero-init is given):
weavec-ccadds-ftrivial-auto-var-init=zeroto the-cc1job, which covers locals, including VLAs. A local whose declaration a jump can bypass (declared in aswitchbody before the firstcase, or skipped by agoto) is not initialised by that option; its pointer uses get null facetsunresolved(no-zero-init).CheckEmitterrewrites everyDeclRefExprto a function whoseLibrarySpecentry has thezero-initflag, unless the program defines a function of that name (§8). A call becomes a call to a prelude wrapper. Taking the function’s address yields a static non-inline wrapper with the same signature, sodo_malloc = mallocis also covered. A unit that defines any function namedmalloc,calloc,realloc,freeor anotherzero-initentry lowers nothing, so an allocator implementation is never wrapped into recursion.
The wrappers call __builtin_malloc, __builtin_calloc,
__builtin_realloc and __builtin_memset, which need no declaration; the
prelude declares only the usable-size query (malloc_size on Darwin,
malloc_usable_size on glibc and musl, both compatible with the system
headers’ declarations). Writing usable(p) for that query:
__weavec_malloc_zero(n):p = calloc(1, n), then zero[n, usable(p)).callocavoids touching fresh pages of large allocations.__weavec_calloc_zero(m, n):p = calloc(m, n), then zero[m * n, usable(p)).__weavec_realloc_zero(p, n):old = p ? usable(p) : 0; q = realloc(p, n ? n : 1), then, ifq, zero[min(n, old), usable(q)). This clears both the tail a shrink leaves behind (a later in-place growth would otherwise expose stale bytes) and the part of a moved block thatreallocdid not copy. Mapping a zero size to one makesrealloc(p, 0)behave the same on every C library.- The same for
reallocarray,aligned_alloc,posix_memalignandmemalign. - Every other
fresh(free)row with thezero-initflag (strdup,strndup,getline,getdelim,asprintf, …) gets a wrapper that zeroes from the end of the bytes the call wrote, as the row’s extent says, tousable(p). Rows markedno-zero-initare not lowered, and their memory falls under A5. alloca(n)with a side-effect-freenbecomes__builtin_memset(__builtin_alloca(n), 0, n). Otherwise it is left as is, and pointers loaded from it areunresolved(no-zero-init).
The invariant is that every byte of a lowered block’s usable region is
either zero or was written by the program, so no realloc can expose a
stale pointer. On any target without a usable-size query,
zero-initialisation of the allocation family is unavailable: weavec-cc
warns once and behaves as -fno-weavec-zero-init for it.
A comparison of a function pointer with malloc (fp == malloc) sees the
wrapper and becomes false. This behaviour change is documented, and
-fno-weavec-zero-init avoids it.
The effect on the guarantee: a pointer read from uninitialised automatic
or lowered heap storage is null, and the null check traps. Probe c12 traps
instead of dereferencing garbage, and probes 08 and 32 become defined
reads of zero. With -fno-weavec-zero-init, every null facet whose
operand may be uninitialised is unresolved(no-zero-init). Memory from
other allocators, from unknown callees and from the program’s own free
lists is covered by A5. The usable-size query is only valid for the
system allocator, or for a replacement that interposes the query along
with malloc; a unit that defines the allocator itself is flagged at link
(§13.2). This assumption is named A5 rather than folded into A3, because
A3 is about code outside U and this one is about U’s own reads.
Amendment (S5). The wrapper is chosen by the row and the declaration,
not by name. malloc, calloc, realloc, strdup and strndup, when
Clang knows the declaration as that library builtin, and the reallocating
rows by their arity (realloc, reallocarray), have wrappers with the
callee’s own signature: the callee reference is swapped, and an address
taken becomes a _fn wrapper. Every other zero-init allocator of the
heap family (aligned_alloc, memalign, valloc, pvalloc) keeps its
call inside __weavec_zero_tail(call, 0), which zeroes the whole usable
region, and a narrow string result keeps its call inside
__weavec_zero_string. An or-fresh row (realpath, getcwd) is lowered
only when the argument that makes its result fresh is a null pointer
constant; getline, getdelim, asprintf and vasprintf only when their
slot is &object; posix_memalign through its helper when the declaration
has exactly the helper’s type. Wide-string rows, alloca of a size with
side effects, references in a unit that defines an allocator, and heap rows
of a freestanding unit or of a target without a usable-size query are not
lowered, and count in A5. Clang’s -ftrivial-auto-var-init=zero already
zeroes __builtin_alloca; the explicit memset stays, and the optimiser
drops one of the two.
12. The ledger: JSON, SARIF, fingerprints and the summary line
Section titled “12. The ledger: JSON, SARIF, fingerprints and the summary line”12.1 JSON schema (weavec-ledger, version 1)
Section titled “12.1 JSON schema (weavec-ledger, version 1)”{ "schema": "weavec-ledger", "version": 1, "producer": {"name": "weavec", "version": "0.11.0", "revision": "abc1234"}, "scope": "unit", // or "program" "root": "/abs/path/to/project", // fingerprint root (§12.3) "config": {"checks": "trap", "zeroInit": true, "require": "none", "budget": 200000}, "summary": { "sites": 4210, "proven": 3050, "checked": 980, "violation": 0, "unresolved": 150, "trusted": 30, "facets": {"spatial": {"proven": 0, "checked": 0, "violation": 0, "unresolved": 0, "trusted": 0}, "null": {…}, "temporal": {…}, "assertion": {…}}, "unresolvedReasons": {"unknown-extent": 70, "…": 0}, "trustedReasons": {"system-api": 12, "…": 0}, "unresolvedShare": {"spatialNull": 0.071}, "errors": 0, "warnings": 2, "functions": 212, "overBudget": ["cJSON_ParseWithLengthOpts"] }, "assumptions": { // program scope only "A1": {"exportedRequirements": 14, "verified": 11, "reliesOnSingle": 40, "unverifiedCallers": 3}, "A3": {"headerInvariants": 2, "unverified": 0, "inputsWithoutRecords": ["liblua.a"]}, "A4": {"concurrencySites": 0}, "A5": {"nonLoweredAllocations": 4, "bypassedDeclarations": 0, "allocatorDefinedBy": null} }, "units": [{ "source": "cJSON.c", "object": "cJSON.o", "target": "arm64-apple-macosx15.0", "summary": {…}, "functions": [{ "name": "cJSON_Delete", "file": "cJSON.c", "line": 253, "linkage": "external", "overBudget": false, "requireSafe": false, "setjmp": false, "sites": [{ "ordinal": 0, "kind": "deref", "line": 258, "column": 21, "text": "item->next", "boundary": null, "callee": null, "facets": { "null": {"outcome": "checked", "check": {"template": "nonnull"}, "fingerprint": "…"}, "spatial": {"outcome": "proven", "fingerprint": "…"}, "temporal": {"outcome": "unresolved", "reason": "unknown-callee", "detail": "'global_hooks.deallocate' may have released 'item'", "fixit": null, "requirements": [], "diagnostic": null, "fingerprint": "…"} } }] }] }], "diagnostics": [{ "id": "use-after-free", "severity": "warning", "certainty": "possible", "message": "use of 'p' after it may have been freed", "file": "cJSON.c", "line": 1, "column": 1, "function": "f", "site": 3, "facet": "temporal", "notes": [{"message": "freed here on some paths", "file": "cJSON.c", "line": 1, "column": 1}], "fingerprint": "…" }]}Field rules:
textis the site’s source text with whitespace removed, truncated to 80 bytes.fileon a function is the root-relative path of the file that holds its definition, which differs from the unit’ssourcefor a function defined in a header. Sites take their function’s file. (Amendment, S3: without it, sites of header functions had ambiguous line numbers.)checkis present on checked facets. In verify mode, proven facets that received a check carry"check": {"template": …, "proven": true}. In that mode everysummaryalso carries"verifyChecks", the number of them, and"verifyCoverage": {"spatial": {"proven": …, "checked": …}, "null": {…}}, the verify coverage of the proven spatial and null facets that gate G6 asks the ledger to report. (Amendment, S8: §10.7 asked the summary to count the verify checks; one number cannot say which proofs are monitored, and the gate is about coverage.)requirementslists, for LibCall, Release and call-site facets, one entry{arg, need, have, outcome, reason?, check?}per requirement that merged into the facet (§2.5). A requirement with its owncheckis enforced even when the merged outcome isunresolved.- Each unit’s
summarycarries"a5": {"nonLoweredAllocations": …, "bypassedDeclarations": …}: the calls to allocation rows that are not lowered and the pointer locals whose declaration a jump can bypass. The program’sassumptions.A5sums them and names a unit that defines the allocator (§13.2). - Paths are relative to
root, with/separators, or absolute when outside it. - Output is deterministic: functions, sites and diagnostics appear in source order, and object keys in the order shown.
12.2 SARIF 2.1.0 mapping
Section titled “12.2 SARIF 2.1.0 mapping”-fweavec-ledger-format=sarif (--ledger-format=sarif) writes one run:
| SARIF | From |
|---|---|
runs[0].tool.driver |
name: "weavec", semanticVersion, informationUri; rules: one per diagnostic id (id = the WeaveC id), one per unresolved/<reason> and one per trusted/<reason> |
runs[0].originalUriBaseIds.SRCROOT |
root |
| result for each diagnostic | ruleId = id; level error or warning; kind: "fail"; message.text; locations[0] (artifactLocation.uri root-relative with uriBaseId: "SRCROOT", region.startLine/startColumn); relatedLocations from notes; partialFingerprints["weavec/v1"]; properties.certainty |
| result for each unresolved facet | ruleId: "unresolved/<reason>", level: "note", kind: "open", message <facet> of <kind> '<text>' is unresolved: <detail>; fingerprint as above |
| result for each trusted facet | ruleId: "trusted/<reason>", level: "none", kind: "review" |
| proven and checked facets | not results; counts in runs[0].properties.weavec.summary |
runs[0].invocations[0].executionSuccessful |
no internal error occurred |
12.3 Fingerprints
Section titled “12.3 Fingerprints”fingerprint = hex(SHA-256(UTF-8("weavec-fp/1" ␟ key ␟ path ␟ function ␟ message ␟ ordinal)))[0:32]␟ is U+001F. The components:
keyis the diagnostic id for a diagnostic, and<kind>/<facet>/<outcome>[/<reason>]for a facet row.pathis the root-relative path. The root is the nearest ancestor of the source file that contains.git, else the compile job’s working directory.functionis the enclosing function’s name, or<file-scope>.messageis the diagnostic message, or for a facet row the site’stext, normalised: every maximal run of ASCII digits becomes0, whitespace runs become one space, and the result is trimmed.ordinalis the 0-based index of the row among rows of the same function with the samekeyandmessage, in (line, column) order.
The fingerprint therefore survives edits elsewhere in the file and function, and changes in sizes and line numbers. RFC 0032’s baselines key on it.
12.4 The summary line
Section titled “12.4 The summary line”One line per TU. weavec always prints it; weavec-cc prints it under
-fweavec-summary or when -fweavec-ledger is given, and is otherwise as
quiet as Clang:
weavec: cJSON.c: 4,210 sites: 3,050 proven, 980 checked, 150 unresolved, 30 trusted; 0 errors, 2 warningsVariants:
- With
-fweavec-checks=none, and inweavec, “checked” readscheckable (not enforced). - Over-budget functions append
; 1 function over budget (<name>).
At link:
weavec: program minigzip: 9,876 sites in 3 units: …; 0 errors, 1 warning; 1 input without a WeaveC record (libz.a); unverified: 12 exported requirements (A1), 0 header invariants (A3)13. Sidecar format 28 and the link step
Section titled “13. Sidecar format 28 and the link step”13.1 Framing
Section titled “13.1 Framing”A unit record is written to <object>.weavec next to the object; the
transport is unchanged from RFC 0005. The file holds exactly one record,
which is self-delimiting so that RFC 0032 can place it verbatim into an
object section:
| Offset | Size | Field |
|---|---|---|
| 0 | 8 | magic 89 57 56 43 0D 0A 1A 0A (\x89WVC\r\n\x1a\n) |
| 8 | 4 | format, little-endian u32 = 28 |
| 12 | 4 | flags, u32 = 0 (readers reject non-zero) |
| 16 | 32 | schema fingerprint: SHA-256 of the codec’s canonical field table (every key, its type and nesting), generated from the same table the encoder and decoder use |
| 48 | 8 | header length H, u64 |
| 56 | 8 | payload length P, u64 |
| 64 | H |
header: UTF-8 JSON |
64+H |
P |
payload: UTF-8 JSON |
64+H+P |
32 | SHA-256 of bytes [0, 64+H+P) |
The header holds producer, source, cwd, command (the -cc1
arguments), target, config (as in §12.1) and
object: {path, digest: "sha256:<hex>"}.
The payload holds these keys:
functions:{name, linkage, addressTaken, typeKey, summary, kinds: {params, result}, reliesOnSingle, requirements}.summaryis SummaryIO format 27 text;reliesOnSinglelists the parameters whose Single default the body relies on (§7.3);requirementsare the inferred exported requirements.globals:{name, typeKey, kind}.imports:{name, declared: {params, result, ownership}, location, calls}, for each external function the unit declares and calls;callslists, per call, whether each pointer argument was Single-valid, so the link step can check reliance across units. Declared annotations and attributes are recorded for link verification.indirect: type keys of indirect calls.unknown: names of callees that were unknown in this TU.slots:{slot, targets, sources, open}, the exported slot constraints; they replace RFC 0014’scallbackGlobals.slotKinds:{slot, kind, demotedBy}, the §7.3 slot kinds and the stores that demoted them.invariants:{struct, field, template, verdict, relied, store}.contexts: the RFC 0016memoryRequestsandcallbackRequests, carried unchanged, so call-context runs keep working across units.countFields: RFC 0010’s count-field keys, carried unchanged.sizedFieldsandsizedFieldLoads: RFC 0012’s witnesses, refutations and loaded keys, carried unchanged. They are not replaced byinvariants, because §7.6 may be cut.boundaries: the place classes of the unit’sdangling-escapeandsecond-ownerrows, for program-wide propagation (§9.4).sites: per function, a compact row per site:[ordinal, kind, line, column, spatial, null, temporal, assertion], each facet as"<outcome>[/<reason>]"ornull.reported:{id, file, line, column}of diagnostics already reported at compile time.definesAllocator: whether the unit definesmalloc,calloc,reallocorfree(§11).
Records carry no evidence: no traces, notes or explanation text. Summary
format 27 is format 26 minus every component that only checked mode reads,
as determined by reachability in S1, plus kind, result-kind, the
lossy flag and the restricted outcome … when cases of §9.1.
Amendment (S8). The implemented payload refines this list; the
codec’s field table (lib/Frontend/RecordPayload.cpp) is authoritative, and
the schema fingerprint changes with it:
functions[]also carrieslocationandcontexts(the RFC 0014 and RFC 0016 specialisations a caller requested), and itskindsspell their source (default single nullable), so the link step can tell a declared kind from a default.imports[].declared.paramshas one object per parameter (name,kind,ownership), and each entry ofcallsis{function, site, args}, so the link step can find the caller’s Call site for reliance and requirement rows.- Further keys:
unknownIndirect(indirect calls whose slot had no target in the unit),slotRules(the unit’s slot constraints before solving),interfaces(exported requirements and reliance flags),a5(the unit’s §11 counts) andcontexts.callbackGlobals. sites[]groups carry the function’sfile,lineandlinkage.- The schema fingerprint also covers the SummaryIO format version, so a summary-format change makes older records stale instead of being skipped.
functions[].requirements[]also carrieselement, the bytes one element of the pointee takes, so acountedterm becomes bytes at a caller that never saw the definition.- Each entry of
imports[].calls[]also carriesevidence, one object per argument:bytes(what the §7.3 judgement guarantees behind a pointer argument, a lower bound; 0 for the null pointer constant),exact(the width is the whole object’s, at offset 0: a complete array, a string literal or&object),null, andvalue(an integer argument’s constant). This is what §13.2 step 5 decides an exported requirement with; kinds and must-access requirements are per-TU and need a body, so a callee defined in another unit has none in the caller’s table, in its compile or in its re-run at link.
Readers accept only format 28 with a matching schema fingerprint and a valid digest. Anything else is a stale record, and the input is treated as having none. There are no legacy readers, and unknown fields are never skipped silently: the schema fingerprint is derived from the codec’s field table, so it changes whenever the payload schema changes, and a round-trip unit test compares the keys the encoder emits with that table.
13.2 The link step
Section titled “13.2 The link step”When weavec-cc links (and weavec --whole-program, which uses the same
ProgramAnalysis):
- Collect inputs.
collectLinkInputs(Driver.cpp) no longer skips an input silently. It sees theInputInfos and, separately, the-larguments, which it resolves the way the linker does: the-Ldirectories in order, then the toolchain’s library directories, then the SDK or sysroot, preferring.tbd,.dylib/.soand.ain the linker’s order for the target. An input is system when it resolves under the toolchain, SDK or sysroot library directories (-lc,crt*.o,/usr/lib, …), the same roots §5.2 uses for headers; a Homebrew-lreadlineis not system. Every non-system input without a valid record (object, archive, shared library, stale record) is named in oneunanalyzed-inputwarning per link, which lists them all. Calls from analysed units into functions no record defines aretrusted(external-unit)when such an input exists, and unknown-callee otherwise. - Solve slots over all records (§9.3).
- Verify declarations. For every import with declared annotations or
kinds, compare against the defining unit’s summary and kinds. A
contradiction is an
annotation-mismatcherror at the declaration. This covers probe 38: a declaration saysWEAVEC_BORROWED, but the definition frees the parameter. - Re-run the engine over every unit with a record (RFC 0005’s algorithm, with the program database and solved slots) to refine temporal facets. Only the last round publishes (§2.6). Definite temporal violations found at link are errors, and the link fails as today; possible ones are warnings. Amendment (S3): a definite spatial or null violation that only the program-wide view reveals (a callee’s effect on an extent, known once its unit’s summary is visible) is also an error at link, so v0.10.0’s link-time catches are kept. The program ledger still copies the unit’s spatial and null outcomes (step 6), because those decided the emitted code; the link error names the site and the unit’s check, if any, stays in place.
- Verify interfaces.
- For every cross-unit call to an exported function with an inferred
requirement, decide the requirement at the caller: proven discharges
the callee’s
trusted(caller-contract)in the program ledger; violation is an error; unresolved stays listed under A1. - For every cross-unit call whose argument for a
reliesOnSingleparameter was not Single-valid (§7.3), add the Call site’sunresolved(unknown-extent)spatial row, as within a unit. - For every header-struct invariant a unit relied on, every unit that
stores to the fields must report
holdsor have no stores.violatedis anannotation-mismatcherror naming the store;unknown, or a unit without a record, leaves the invariant listed under A3. A header slot that is Single in one unit and demoted in another is listed under A3 likewise. - Propagate boundary rows program-wide (§9.4).
- If a unit defines the allocator and another unit lowered allocation
calls (§11),
weavec-ccprints one driver warning, “heap zero-initialisation assumes the system allocator, but '<unit>' defines 'malloc'; rebuild with -fno-weavec-zero-init”, and the ledger records the unit under A5.
- For every cross-unit call to an exported function with an inferred
requirement, decide the requirement at the caller: proven discharges
the callee’s
- Compose the program ledger. Spatial, null and assertion facets are
copied from the unit records, because they decided the code; the Call
rows of step 5 are added. Temporal facets come from step 4. The ledger
is written under
-fweavec-ledger, and the summary line is printed under-fweavec-summaryor when-fweavec-ledgeris given.
The link step verifies A1 and A3 only where the records cover the callers and the stores; everything else stays listed under those assumptions in the summary line.
Amendment (S8): how step 5’s first bullet is decided.
verifyRequirements (LinkStep.cpp) decides each exported requirement at
each cross-unit call from the caller’s record alone, because kinds and
must-access requirements are per-TU and need a body: a callee defined in
another unit has none in the caller’s KindTable, neither in its compile
nor in its re-run at step 4. The evidence is imports[].calls[].evidence
and functions[].requirements[].element of §13.1.
- The guard is evaluated first: when it is known not to hold, the
requirement is met, because a loop that runs zero times accesses nothing
(§7.5 R2).
countedandsizedterms then become bytes with the overflow-checked arithmetic of §10.2;ended-byandnul-terminatedare not decidable from a record and stay unresolved. - The argument’s width is a lower bound, so it proves the requirement when
it covers the need, but a shorter width is a violation only when the
record says the width is the whole object’s (
exact: a complete array, a string literal or&object) and the guard is known to hold. A null argument under a non-empty requirement is left unresolved, since §7.5 never trusts the nullability part. - The error for a violation is step 4’s, not step 5’s: the caller’s re-run
sees the callee’s summary requirement (RFC 0011) with the whole
dataflow in view, and reports
out-of-boundsat the argument itself. Step 5 adds the ledger’s Call row (arg,need,have) beside it, so the two are never reported twice. verifiedcounts the requirements met at every cross-unit call the records contain. A function all of whose requirements are met that way has itstrusted(caller-contract)rows replaced byprovenin the program ledger, but only when the program is closed for it: no link input without a record, an executable link that is not-rdynamic, an address that is not taken, its own unit analysed at link, and no call to it inside that unit left unresolved or violated.
Static archives, shared libraries and ccache still do not carry records; that is RFC 0032. The difference now is that the gap is named at every link.
14. The engine seam
Section titled “14. The engine seam”Everything an engine produces flows through one interface. RFC 0031 can
therefore replace FunctionDataflow by implementing SafetyEngine,
without touching the ledger, kinds, library table, planner, emitter,
formats or tests.
namespace weavec::analysis {
/// Everything an engine gets for one unit.struct EngineInput { clang::ASTContext &context; const SiteIndex &sites; // SiteCollector: Expr/Stmt -> SiteId, per function const KindTable &kinds; // declared + must-access + slot kinds (§7) const core::LibrarySpec &library; const core::FnSlots &slots; // local or program-wide solution (§9.3) const ProgramDatabase *database; // other units' records, at link; else null const std::vector<FieldCandidate> &fieldAssumptions; // §7.6 candidates assumed at entry EngineOptions options; // budget, zeroInit, require, verify, strictAliasing, dumpStream, stats};
/// The only channel from an engine to the ledger.class LedgerAdapter {public: /// Starts the authoritative pass over `function` and discards every row an /// earlier pass recorded for it (§2.5, §2.6). Houdini rounds and fixpoint /// rounds publish into a discarding adapter instead. void beginFunction(const clang::FunctionDecl &function); /// One decision for one facet of a known site; may be called more than once /// within a pass (§2.5). void decide(const clang::Stmt &site, core::Facet facet, core::SiteOutcome outcome, std::optional<core::UnresolvedReason> unresolved = {}, std::optional<core::TrustReason> trusted = {}, std::string detail = {}); /// A diagnostic with its certainty; `site`/`facet` link it to its row. void report(core::Diagnostic diagnostic, core::Certainty certainty, const clang::Stmt *site = nullptr, std::optional<core::Facet> facet = {}); /// For a checked spatial or null facet, and for a proven one when /// `EngineOptions::verify` is set: what the check needs (extent, base, /// offset, lengths, whether the extent is exact or declared). void witness(const clang::Stmt &site, core::Facet facet, CheckWitness witness); /// Caller-visible places at a call or exit that may hold released pointers /// or aliased owners (§9.4). void boundary(const clang::Stmt &site, BoundaryFacts facts); /// A function body exceeded its budget (§5.5). void overBudget(const clang::FunctionDecl &function); /// Store verdicts for field-invariant candidates (§7.6). void storeVerdict(const clang::Stmt &store, FieldCandidate candidate, core::Verdict verdict); /// Fills defaults (§2.6), applies BoundaryInvariants and their propagation, /// the UNSAFE/setjmp/concurrency overrides and the §7.6 upgrades, runs /// CheckPlanner::plan (§10.1), and returns the unit's ledger and plan. The /// require-level errors are derived from the result. [[nodiscard]] PlannedLedger finish();};
/// What RFC 0031 implements.class SafetyEngine {public: virtual ~SafetyEngine(); /// Analyse every emitted function of the unit, publishing through `out`. virtual void analyzeUnit(const EngineInput &input, LedgerAdapter &out) = 0; /// Summaries and exported facts for the unit record (§13.1). [[nodiscard]] virtual UnitExports exports() = 0; /// `--dump-analysis` text for one function (unstable format). virtual void dump(const clang::FunctionDecl &function, llvm::raw_ostream &os) = 0;};
} // namespace weavec::analysisThe auxiliary types:
CheckWitnesscarries place handles and terms: the object base; the extent term (withscale,offsetand the place handle); the offset term or index; for LibCall sites, the per-argument lengths; and, when the pointer was loaded from a field, that field and the object it was loaded from (§7.6 Application). Handles are resolved againstEngineInput::context.BoundaryFactslists, as summary paths from the boundary’s parameters and globals, the places that may hold a released or dead pointer (each with its release location), and the pairs of owning places that may hold the same object, each with its place class for propagation (§9.4). The engine also marks, per temporal decision, the place class whose entry assumption it relied on.FieldCandidateis{record type key, pointer field, integer field, template}.KindTableholds, per parameter, result and slot, the declared or inferred kind with itsKindSource, whether it is exact, declared or a lower bound (§7.1), and thereliesOnSingleflags.PlannedLedgeris{core::Ledger ledger; core::CheckPlan plan;}.core::Certaintyis{Definite, Possible}.core::Verdictis{Holds, Violated, Unknown}.
Two rules keep the seam honest:
FunctionDataflowpublishes nothing except throughLedgerAdapter, including its diagnostics:DiagnosticSinkis no longer passed to it.- The new components (
SiteCollector,AttributeReader,KindInference,SlotCollector,BoundaryInvariants,CheckPlanner,LedgerAdapteritself) never includeDataflow.h.
Gate H2 greps for both.
15. Changes inside FunctionDataflow
Section titled “15. Changes inside FunctionDataflow”The edits are bounded to this list. Anything beyond it needs an amendment:
- S1: delete the checked hooks. This covers
checkedBefore/checkedAfter, about 85checkContractsbranches, about 50 per-CallExprpost tables, theSafetyStateoptional, the public checked members (checkedOutputClasses,recursiveContractCalls, …), andAnalysisOptions::{checkContracts, checked, checkedMainFileOnly, checkedFunctions, deferCheckedCalls}.AnalysisOptions::{reportUnannotated, strictExterns, exclusiveBorrows}go in S3, with the sound defaults, so that S1’s output stays byte-identical to the golden run. - Route diagnostics through the adapter.
report()andflushDiagnostics()go throughLedgerAdapter::reportwith a certainty, and each diagnostic site also callsdecidefor its facet. The call sites arereportUseOfMoved, the double-free branch,checkDereference,checkResultDereference, the argument-null check, the bounds reporter,reportLeakand the RFC 0007/0008 ids. - Replace
reportIncompletewithdecide(site, facet, Unresolved, reason, detail). The reason strings map tobudget(“… limit reached”),unanalysed(“unsupported …”, “unresolved array …”, “… is ambiguous”),raw-cast(“incompatible or unknown object view …”, “unsupported memory copy of pointer-containing storage”) andinexpressible(“unrepresentable …”). The original string is kept asdetail. - Publish decisions in the final pass. In
Phase::Final, publish proven, checked and unresolved decisions for every Deref, Index, LibCall, Release, Call, Cast and PtrArith site the pass reaches, using the null, spatial and temporal rules of §3, and attachraw-castto the Deref and Index facets of pointers the engine knows were created by reinterpretation (union punning, byte-wise copies,va_arg). - Certainty bits. Add the
MoveRecord,NullRecordandLoanbits of §3, and their setting and clearing. - Unknown callees. Remove the
annotation-requiredpaths and the system-header early return; apply the §5.1 effects. - Library calls. Consult
LibrarySpecinstead ofBuiltins,RuntimeModelsand thename ==tests (S4). inUnsafe. Its suppression becomes the §6.1 decisions.- Assumptions. Refute
WEAVEC_ASSUMEintocontradicted-assumptionand publish the assertion decision. - Budget counting.
- Special cases:
setjmpdetection, callback application, and concurrency marking from theGset computed before the unit’s analysis. - Boundary facts and witnesses. Publish
boundary(…)at calls, callback LibCalls and exits (including non-returning calls), with the place class of each fact, andwitness(…)for checked facets and, in verify mode, for proven spatial facets. - Summary derivation. Add the case translation and
lossybit (§9.1), the non-null-from-guard rule with the completeK(§9.2), parameter kinds and result kind in the summary, and the “always returns” fact must-access inference needs (§7.5). - Consume kinds. Seed the engine’s
KnownExtentfromEngineInput::kindsat parameter entry, at field and global loads and at call results: Single gives the object width of the pointee, andCounted(e)giveseelements witheas a place. Each seeded extent carries whether it is exact, declared or a lower bound, and the spatial decision maps a lower bound tounresolved(unknown-extent)for any access it does not cover (§3.3). Without this,p->fon a parameter stays unresolved and gate G13 cannot move. - Object extents. Change
DataflowDynamicExtentsto the §7.4 object rules: flexible trailing arrays, complete-object extents for pointers made from members and elements, and the arithmetic model. - Requirements. Keep
requiresNonNull/requiresExtentas may-facts for summaries only, drop themarkDereferencedcall incheckRequiredArgumentsfor arguments of unknown nullness, and apply the refinement rule of §3.2. - Temporal rules of §3.1. Replace an
unknownOriginrecord on a known consume, and publishmay-alias-releaseddecisions. - Passes. In
TranslationUnitAnalyzer::run, callbeginFunctionfor the authoritative pass of each function (§2.6); run a generic reporting pass for functions that today report only through context runs; foldreportConfirmedSizedFieldsinto it; keep context-run diagnostics and link them to the caller’s Call site; and send the callback-global and SCC rounds to a discarding adapter. The same applies toProgramAnalysis’s link rounds. The RFC 0014 callback-global fixpoint is replaced byFnSlots(§9.3). - Incomplete summaries. Apply §5.5’s rule at every use of a summary
marked incomplete or produced when
MaxFixpointRoundswas exhausted.
Context-specialised runs publish summaries and diagnostics, never rows of
the function they analyse. The generic pass added by item 18 costs CPU on
Lua, where many functions report only through contexts today; gate G15
budgets it, and §5.5 bounds the context runs. Gate H2 requires
Dataflow*.{h,cpp} to be at most 25K lines after these changes, down
from 41.1K.
16. Command-line interface
Section titled “16. Command-line interface”weavec-cc flags:
| Flag | Default | Meaning |
|---|---|---|
-fweavec / -fno-weavec |
on | analyse, check and zero-initialise; off is plain Clang |
-fweavec-checks=trap|report|verify|none |
trap |
§10.7 |
-fweavec-zero-init / -fno-weavec-zero-init |
on unless checks are none |
§11 |
-fweavec-require=none|checked|proven |
none |
§6.3 |
-fweavec-ledger=<path> |
unset | write the unit ledger (compile) or program ledger (link). A value ending in / or naming a directory writes <dir>/<object>.ledger.json per unit and <dir>/<output>.ledger.json at link, which is the form to use in CFLAGS under make -j; otherwise the file, and it is an error if one invocation would write two ledgers to it. Every ledger is written to a temporary file and renamed into place, so parallel writers never interleave |
-fweavec-ledger-format=json|sarif |
json |
§12 |
-fweavec-summary / -fno-weavec-summary |
off (on when a ledger is written) | the summary line on stderr |
-fweavec-budget=<n> |
calibrated (§5.5) | per-function block-transfer budget; 0 = unlimited |
-fweavec-link / -fno-weavec-link |
on | kept |
-fweavec-dump-analysis, -fweavec-analysis-stats=<path> |
off | kept as debugging flags |
-fweavec-print-prelude |
off | print the check prelude for the current -fweavec-checks mode and exit (used by the rewrite-oracle tests, G8) |
-W[no-][error=]weavec[-<id>] |
— | kept; -Wweavec-allocation-failure enables the off-by-default id |
weavec flags:
- Kept:
--whole-program,-p <dir>,--dump-analysis,--analysis-stats=<path>and the-Wflags. - Added:
--ledger=<path>,--ledger-format=json|sarif,--require=none|checked|proven,--budget=<n>and--no-zero-init. The ledger models aweavec-ccbuild with the default checks. - The summary line is always printed.
Removed from both tools, with no aliases:
--checked,--checked-function,--checked-report,--checked-report-format;-fweavec-checked,-fweavec-checked-function=,-fweavec-checked-report=,-fweavec-checked-report-format=;--exclusive-borrowsand-fweavec-exclusive-borrows(RFC 0006’s full exclusivity rule goes with them);--strict-externsand-fweavec-strict(subsumed by §5 and the require levels);--analysis-cacheand-fweavec-analysis-cache=;--analyze-headersand-fweavec-analyze-headers(§5.6);--report-unannotatedand-fweavec-report-unannotated(replaced by ledger fix-its).
An unknown -fweavec-* flag is an error, as today.
Two crashes are fixed; the second has a different cause in each tool:
weavec --whole-program -p buildsegfault.tools/weavec/main.cppkeepsCommonOptionsParser, and with it--,--extra-argand--extra-arg-before. The defect is the early return underZeroOrMorewhen no source is named, after whichgetAllFilesdereferenced a null database. When no source is named and-pis given, the tool now loads the database withCompilationDatabase::autoDetectFromDirectoryand takes its files. A missing database is an error with a message.-fdiagnostics-format=sarifcrash inweavec-cc. DeletingInputIdentityremoves the re-preprocessing path throughSarifDocumentWriter(Driver.cpp:227/321). The link step’s internal re-analysis forces-fdiagnostics-format=clang, because its diagnostics are relayed through the link’s own engine in the user’s format.- The same crash in
weavec. Here the crash is inClangTool::run(SarifDocumentWriter::createRununderBeginSourceFile), because the tool’s runs are the user-visible ones and no writer is attached.weaveccreates the diagnostics engine for its runs itself and, when the flag is given, installs Clang’s SARIF printer with aSarifDocumentWriterthat it owns and closes after the last run. If that proves infeasible in S3, the flag is rejected with an error pointing to--ledger-format=sarif; either way the tool no longer crashes (H3). The WeaveC ledger has its own SARIF writer in both tools.
Both fixes get lit tests (gate H3).
17. Tests
Section titled “17. Tests”17.1 The golden oracle
Section titled “17.1 The golden oracle”weavec and weavec-cc built from e0e2bd6 (v0.10.0), out of tree, are
the golden oracle for parity. test/cases/GOLDEN.md records the commit,
the build recipe and the expected location, which is taken from the
WEAVEC_GOLDEN_DIR environment variable. The golden binaries are not
committed.
17.2 test/cases
Section titled “17.2 test/cases”One tree, organised by feature, replaces test/evaluation, test/recall
and the per-RFC populations:
test/cases/ README.md marker grammar and runner usage GOLDEN.md KNOWN-DIFFERENCES.md lit-pin misses versus the golden run (at most 25 entries, gate G3) evaluation/ the 44 bug + 32 clean programs (from test/evaluation) pairs/ the 24 RFC 0017 cases (12 bug/clean pairs) recall/<CWE>/ the recall pins (from test/recall; 67 as scripts/recall.py counts them) engine/ ordinary-era lit engine pins, reduced to (line, id) from the golden run soundness/ the 113 probes (85 bug, 28 correct) and their *_impl.c units repros/ the 12 root-cause repros (realloc0–5, luaalloc, pred, zerolen, mainleak, arrloop, loopcorr) proofs/ salvaged population cases that ever caught a false proof; SOURCES.md gives origin paths and commits semantics/<feature>/ new cases: ledger, unsafe, assume, require, kinds, extents, library, slots, boundary, aliasing, concurrency, zero-init, emissionImporting a case adds a main driver, in S0, when its pinned bug is a null
or spatial finding that becomes checked. This lets the executable oracle
observe the trap. The frozen-hash protocol is gone, and cases are edited
like any source.
The review of this RFC produced cases that semantics/ must contain, each
pinning a rule above: trailing-array and sub-object extents (Lua’s
upvals[1], memcpy into &ts->contents, memset through a first
member, a flat walk of m[3][4], §7.4); a qsort comparator, p + 1
dereferenced in a callee given int[2], and a sentinel read p[n]
(§7.1, all clean); two(p, p) and b->cur aliasing b->data (§3.1);
get(a + 4, …) into a static function and a cursor stored through a
char ** (§7.3); remember(p); free(p); peek() and free(g); exit(0)
with an atexit reader (§9.4); realloc(p, 0) (§8.2); an empty vector’s
memcpy(NULL, NULL, 0) (§8.3); a negative count field and a wrapping
qsort size (§10.2); a zero-trip loop, a size_t i - 1 loop and a call
that exits before the access (§7.5); a cast end sentinel and a
flexible-array struct smaller than sizeof (§7.4); the ok() guard
(§9.2); a lossy wrapper (§9.1); a nonnull-only destructor (§5.1); a
handler racing a null test and a worker using a freed global (§5.3); a
C99 inline definition inlined at -O2 (§2.6); fp = memcpy (§8); and a
-Wno-error-lowered out-of-bounds store that must trap (§3.4).
Salvage for proofs/ covers three families: the reader cursor, p + 1
forwarding, and the candidates 99–102 leaks. It also covers every
population case whose expected outcome changed from accept to reject in
git log of test/evaluation/rfc00NN/**/manifest.json.
17.3 Marker grammar
Section titled “17.3 Marker grammar”Markers are line comments. A line marker applies to the line it is on; a file marker appears before the first declaration.
// CLEAN file: no errors, no warnings (except ALLOW), no traps// ALLOW: <id> [<id> …] file: warnings with these ids do not fail CLEAN// BUG: <id> [definite|possible] line: a diagnostic with <id> here; definite = error, possible = warning,// omitted = either; for null/spatial ids a TRAP on the same line also satisfies it// TRAP: <template> line: running the program traps here with nonnull|index|span|len|disjoint|assert|violation// RUN-INPUT: <argv…> [< <file>] file: run the program with these arguments (repeatable; each run independent)// UNRESOLVED: <facet>:<reason> line: a ledger row here has that facet unresolved with that reason// TRUSTED: <facet>:<reason> line: likewise, trusted// NOT-PROVEN: <facet> line: no ledger row here has that facet proven// NEUTRALISED: zero-init line: the defect is defined away by zero-initialisation (§11)// MISS: <reason text> line: a known miss; counted in the denominator, expected silent// EXPECT-LEDGER: <json-pointer> <op> <value> file: e.g. /summary/unresolved <= 3; op is == != <= >= < >// FLAGS: <weavec-cc flags> file: extra flags for every compile and link (e.g. -fweavec-require=checked)// UNITS: <file.c> [<file.c> …] file: further translation units linked with this one// ASAN file: also run the ASan oracle// TOOL file: analyse with `weavec` (no build, no run)Facet names are spatial, null, temporal and assertion; reasons are
the §2.3–2.4 spellings. For G4, the runner maps a BUG id to its
matching facet:
use-after-free,double-free,use-after-move,conflicting-borrow,lifetime-too-short,mismatched-releaseandannotation-mismatchmap to temporal;null-dereferenceanduse-of-uninitializedmap to null;out-of-boundsandinvalid-releasemap to spatial;contradicted-assumptionmaps to assertion.
17.4 The runner
Section titled “17.4 The runner”scripts/run-cases.py options:
--weavec-cc PATHand--weavec PATH(default:build/<preset>/bin);--jobs N(default: CPU count);--checks trap|verify(defaulttrap);--require none|checked|proven(adds-fweavec-requireto every build; used by G5);--asan(run the ASan oracle for every case, not only// ASANones);--legacy(golden semantics: diagnostics only, no ledger);--no-emission(for S3, S4 and fallback points A and B, before checks are emitted: a checked matching facet at the pinned line satisfies a null or spatialBUGmarker, as in gate G3, andNEUTRALISEDmarkers count asMISS);--no-run(build, diagnostics and ledger only; used by the ASan CI job, gate H1);--compare-golden(also runs the golden binaries and fails on any difference in the sorted diagnostics; the S1 gate). From S3 on, cases whose golden run used a flag this RFC removes are excluded and listed in the Excluded section ofKNOWN-DIFFERENCES.md(§17.6);--filter GLOB;--json OUT.
Each case is processed in these steps:
- Build.
- Compile each unit with
weavec-cc -c <FLAGS> -fweavec-ledger=<tmp>/, then link the units withweavec-ccinto<tmp>/a.outwhen some unit definesmain. - With no
main, compile only. Several units withoutmainare analysed withweavec --whole-program. // TOOLcases useweavec --ledger.- Timeouts are 120 s per compile or link.
- Compile each unit with
- Diagnostics. Parse
file:line:col: error|warning: … [weavec::<id>]from stderr. EveryBUGmust match a diagnostic by line and id with the right severity class, unless aTRAPon the same line matches at step 4. For aCLEANcase, any error, or any warning whose id is not inALLOW, fails the case. In any case, an error on a line without aBUGmarker fails it. - Ledger. Check the
UNRESOLVED,TRUSTED,NOT-PROVENandEXPECT-LEDGERmarkers against the unit and program ledgers. - Run, when the build produced
a.out:- Run the trap-mode binary once per
RUN-INPUT, or once with no arguments, with a timeout of 10 s. It must end by a trap signal (SIGTRAPorSIGILL) if and only if the case has aTRAPmarker. - Rebuild with
-fweavec-checks=report, run again, and parse theweavec: runtime check failed: <template> at <file>:<line>:<col>lines. EveryTRAPmust match by line and template, and a reported check failure on an unmarked line fails the case.
- Run the trap-mode binary once per
- ASan oracle (
--asanor// ASAN).- Build with
weavec-cc -fno-weavec -fsanitize=address -g, which is plain Clang withweavec.h, and run.-fweavec-checks=nonewould still fail on definite errors, so the probes projected as new errors could never be validated. - A soundness bug probe must produce an ASan report, which validates the probe. The report’s first in-case frame is the bug site: the ledger row there must not have the matching facet proven (G4).
- A correct twin must run clean.
- Build with
- Verify. With
--checks verify, anyweavec.proventrap fails the case, whatever its markers (G6).
Every bug probe is classified as error, warning, trap, row (a
non-proven matching facet or UNRESOLVED/TRUSTED match), neutralised,
miss or silent. The results JSON gives per-suite tallies of these
classes and per-case failures. The exit status is non-zero on any failed
case. --legacy interprets the same markers against the golden binaries;
it is how S0 proves the runner reproduces v0.10.0’s numbers.
CTest registers one cases-<suite> test per top-level directory of
test/cases, so ctest -j runs the suites in parallel. CI runs them in
the Linux Release job in full, and in the ASan job with --no-run.
17.5 The corpus gate
Section titled “17.5 The corpus gate”scripts/corpus-gate.py reads test/corpus/:
manifest.json. Per project:url, a 40-hexsha(pinned; no branch names),supportfiles, and a list ofconfigs. The 11 configs are sds, cJSON, jsmn, log.c, printf, linenoise, cJSON-program, zlib, lua, linenoise-program and jansson. Each config has:compile: files and arguments for per-fileweavec-cc -c, orbuild: shell commands run withCCset to the binary under test;test: the project’s own test commands (cJSON’s tests, jansson’s test suite, zlib’smake test, a named Luatestessubset, sds’ssds-test, jsmn’s tests);bench: a command, an input and a repeat count;wholeProgram:truefor configs also analysed byweavec --whole-program.
expected.json. A ratchet per config:errors,warnings, ledger counts,unresolvedShare.spatialNull,cpuSeconds,workCounters(block transfers, functions, sites),trapsandoverhead. The gate fails when a count or share gets worse, when CPU time exceeds the recorded value by more than 10%, or when a work counter exceeds it by more than 2%.--updaterewrites the file, and CI fails if the committed file differs from what the run produced, so improvements are ratcheted in by the PR that makes them.triage.json. A list of{fingerprint, config, id, certainty, file, line, verdict: "true"|"false", note}. Every definite error and every possible temporal warning in a run needs an entry. Afalseverdict on a definite error fails G9. An untriaged finding fails the gate. Stale entries are reported but do not fail it. Abuildconfig with a triaged-true definite error lists-Wno-error=weavec-<id>for that id in itsmanifest.jsonentry, next to the fingerprint; no other lowering is allowed. The lowered site traps (§3.4), and a trap there in the project’s test suite is a true positive, not a G11 failure. In per-filecompileconfigs a definite error is recorded as a finding of its TU, not as a build failure.injections/injections.jsonplusinjections/<project>/*.patch. About 30 entries of the form{id, config, patch, file, line, expect: {ids, severity} | {trap, run}, mode: "unit" | "whole-program"}. They include both Lua allocator bugs (theluaM_freeuse-after-free and the doubleluaM_freearray) in whole-program mode, and at least one entry per project.bench/holdslua-bench.lua, thecjson-bench.cdriver and the zlib bench input generator.
Modes:
--quick: per-file compiles and whole-program analyses only. It runs on every PR.--full: adds project builds, test suites, injections and benchmarks. It runs weekly and for the release.--inject: applies each injection patch, rebuilds and checks the expectation (G12).--bench: runs the benchmarks only (G14).--checks verify: runs every project test suite in verify mode (G6).--legacyand--compare-golden: as for the runner (S0 and S1).--update: rewritesexpected.json.
The reference compiler is $WEAVEC_LLVM_PREFIX/bin/clang, used for
overhead baselines and for G7. scripts/codegen-identity.py
implements G7 over test/corpus/identity.txt (at least 100 TUs, none of
which has a definite error, since such a TU produces no object).
scripts/check-hygiene.py implements H2. scripts/corpus.py,
scripts/corpus/projects.json and baseline.json are replaced, and
scripts/corpus/support/ (jansson’s configuration header and the other
support files) moves to test/corpus/support/.
17.6 Existing lit and unit tests
Section titled “17.6 Existing lit and unit tests”Lit tests outside rfc0018-* … rfc0029-* are kept, and each kept test
has one copy: an engine pin converted into test/cases/engine is deleted
from test/Analysis, test/Annotations or test/WholeProgram, and the
rest stay where they are. About 36 kept lit files and 74 unit-test
references across 12 files (DiagnosticControlTest, ArrayOwnershipTest,
PointerIdentityTest, …) name removed ids or messages:
- In S3, their
CHECKlines and expectations are updated to the new messages, severities and ids. - Tests of removed flags are rewritten for the replacement behaviour:
--strict-externsand--report-unannotatedbecome ledger fix-its and require levels;--analyze-headerstests are inverted, since headers are always analysed (headers-skipped.cbecomesheaders-analysed.c);--exclusive-borrowstests are deleted. The 11 affected files (headers-skipped.c,rfc0002-borrows.c,rfc0016-clean.c,version.c,rfc0004-posix.c,ownership-annotations.c,rfc0003-unknown-extern.c,rfc0016-composition.c,rfc0017-numeric.c,rfc0004-function-pointers.c,rfc0005-flags.c) are excluded from the G3 denominator and, once S3 removes the flags, from--compare-golden. They are listed in an Excluded section ofKNOWN-DIFFERENCES.md, which does not count toward G3’s 25 entries. - New tests are named by feature (
test/cases/<area>/…,test/Emission/<feature>-*.c); existingrfcNNNN-names may stay.
18. Deletions
Section titled “18. Deletions”Library code is deleted by reachability in S1. The order is: options first, then the guarded branches, then whatever becomes unreferenced. The list below gives the expected result; the S1 gate decides.
| What | Approx. lines |
|---|---|
lib/Analysis: DataflowSafety*.cpp (5), DataflowContainer*.cpp (3), DataflowFootprints, DataflowBuffer*.cpp (3), DataflowRecursive*.cpp (3), RecursiveContracts.cpp, DataflowCursors, DataflowStringTraversal, DataflowTraversalRelations, DataflowSpans, DataflowByteContents, DataflowFloating, FloatingCastSupport.{h,cpp}, DataflowCallbackContracts, DataflowCheckedCallbacks, DataflowCases, DataflowUnions, DataflowRuntime, DataflowObjectTypes, InterfaceTypes.{h,cpp}, and the residue that becomes unreferenced |
~18.1K (17.2K in Dataflow*) |
Checked hooks and guarded branches in Dataflow.{h,cpp}, PlaceBuilder, Summaries, TranslationUnitAnalysis, CallbackSummaries, CallContextSummaries, SummaryDependencies |
~3.5K |
The RFC 0014 callback-global fixpoint, replaced by FnSlots (§9.3) |
~0.4K |
lib/Analysis/Builtins.cpp, RuntimeModels.{h,cpp}, the name == libc tests |
~1.5K |
Core: Safety.{h,cpp}, CheckedContract.cpp, CheckedIO.{h,cpp}, SafetyEntryPool, SafetyCallPath, Container, Footprint, Buffer, Union, Traversal, the checked and legacy parts of Summary/SummaryIO, SpatialOutcome/SpatialReason |
~6.5K |
Frontend: CheckedReport, CompactReport, CheckpointExplanations, CheckedArtifacts, AnalysisCache, InputIdentity (about 1.4K in the named files and their headers), and the checked parts of Driver, ProgramAnalysis, Sidecar, tools/weavec |
~2K |
Unit tests for the deleted code (SafetyTest, ContainerTest, BufferTest, TraversalTest, UnionTest, FootprintTest, RecursiveC*Test, CheckedCodeTest, RuntimeContractsTest, UnionCasesTest, AnalysisCacheTest, CheckedReportTest, InterfaceTypesTest, the checked parts of others) |
~12K |
Lit tests for superseded RFCs (test/*/rfc0018-* … rfc0029-*, 105 files) |
~1.4K |
test/evaluation/ (after import: 1,959 files, including 1,431 C fixtures, 153 manifests, 161 hash inventories and 114 provenance files) and test/recall/ (moved) |
~39.5K |
scripts/checked-*.py (14), test_checked_evaluation.py, incremental-evaluation.py, scalability-evaluation.py, generate-recursive-oracle.py, evaluate.py, recall.py, corpus.py and their tests, and the 243 population CTest registrations |
~4.2K |
scripts/corpus/rfc00NN-results.json, rfc00NN-diagnostics.jsonl, rfc0014.json, rfc0015.json, baseline.json, scripts/corpus/README.md |
~283K (generated) |
docs/validation-rfc0014.md … rfc0029.md, docs/checked-code.md, docs/incremental-analysis.md, docs/development-history.md, docs/examples/checked.c, and the per-RFC changelog prose in README.md |
~8K |
Kept, although named like checked code. The RFC 0017 integer files are
ordinary-mode code, called unconditionally from the transfer functions, and
stay: DataflowNumericInputs, DataflowNumericOutputs,
DataflowIntegerProofs (operationDoesNotOverflow),
DataflowLoopRequirements, DataflowGuardCompleteness and
DataflowCheckedIntegers (the __builtin_*_overflow calls), about 1.4K
lines, with their unit tests (LoopRequirementsTest,
GuardCompletenessTest, NumericInputsTest). Only their checkContracts
branches go. lib/Core/CheckedInteger.cpp is kept too: it is RFC 0017’s
integer-overflow evaluator.
The line budget. Library code is 77.2K lines today. The deletions
above remove about 32.0K from lib, include and tools. The authored
code is estimated at about 16.3K lines: the design’s 17.5K, less the Core
JSON and SHA-256 implementations that the Frontend codecs no longer need
(§1). That gives about 61.5K against gate H2’s 62K. The margin is small,
so S8 measures it before the documentation work, and any overrun is closed
by deletion, never by raising the gate. Dataflow* lands at about 24K
against its 25K cap.
Amendment (S7). Two rows of the table above were not deleted, because
reachability decided otherwise, as the paragraph before it says it would:
lib/Analysis/InterfaceTypes.{h,cpp} is still used by ProgramDatabase,
Summaries and Core/Interface, and lib/Core/Traversal.cpp (119 lines)
by Dataflow.h, three Dataflow* files, AnalysisState and Summary.h.
Everything else named there is gone, verified file by file. The last two
test rows were still outstanding when S7 began and were deleted then:
test/evaluation/ (1,959 files) and test/recall/ (35), both fully
migrated to test/cases/, and the 22 scripts of the row below them, 2,016
files and 45.4K lines in all, with their CTest registrations.
The line budget above did not survive the same check. Measured after S6
and the performance work, against
the RFC’s base commit: the deletions landed as planned and then some,
34.6K lines removed from lib, include and tools (27.3K of that whole
files) against the 32.0K estimated above. The authored code is 41.4K
lines, against the 16.3K estimated. The estimate was wrong, not the code:
it was taken from the design sketch and never re-derived once §8 (the
library table, 4.5K), §9 (slots, 2.4K), §12–§13 (records, summaries and
the link step, 11.1K) and §6–§7 (pointer kinds, 7.0K) were written out in
full. Library code stands at 82.4K lines in 193 files, Dataflow* at
24.9K in 32.
Holding 62K would mean deleting specified, working functionality, so this
amendment retracts the sentence above that says an overrun is closed by
deletion and never by raising the gate: the estimate behind that number
does not survive measurement, and a gate that can only be met by removing
what the RFC mandates measures nothing. What the cap is for — stopping
the engine growing into an unreviewable mass — is carried by the
Dataflow* sub-cap, which the implementation has held throughout. The
new caps are set from the measurement with headroom for S7 and S8, and
they are ratchets: scripts/check-hygiene.py lowers them as code is
deleted, and raising one again takes an RFC amendment that records the
measurement, as this one does. lib/Core/LibrarySpec.txt no longer
counts against the code cap. It is a declarative table (§8.1), its growth
is library coverage rather than sprawl, and counting it would price
coverage against a budget meant for code; check-hygiene reports its
entry count separately.
Diagnostic ids removed: analysis-incomplete, annotation-required,
checking-incomplete and checking-failed. Annotation removed:
WEAVEC_CHECKED.
19. Documentation, RFC statuses, release tooling and CI
Section titled “19. Documentation, RFC statuses, release tooling and CI”RFC statuses:
- RFCs 0018–0029 become Superseded. In each, the
- **Status**:line becomesSuperseded, the- **Supersedes / superseded by**:line becomesSuperseded by RFC 0030, and a first line> Superseded by [RFC 0030](0030-prove-or-trap.md).goes under the status block; the text is otherwise unchanged. The docs site reads the status from that line (docs/scripts/prepare-content.mjs). RFC 0029 is superseded while Accepted: its cost blocker is closed by deletion, not resolved. - RFC 0001 stays Accepted. A note under its status block and in its Soundness guarantee block points to RFC 0030, Soundness. Its model (kinds, moves, loans, lifetimes) is unchanged.
- Amendment notes, one short blockquote each, are added where earlier
RFCs say something this RFC changes:
- 0002: default severities;
- 0003:
annotation-required, the unknown-callee default,--report-unannotatedand--strict-externs; - 0004:
WEAVEC_UNSAFEsuppression,--strict-externsand itsunsafe-operationforms, boundaries; - 0005: sidecar format, the link step,
--analyze-headers, and stale sidecars, nowunanalyzed-input; - 0006:
--exclusive-borrows; - 0007: leak severity and main-exit leaks;
- 0008: null and uninitialised severities,
annotation-required; - 0011: may-out-of-bounds errors;
- 0012:
WEAVEC_ASSUMEtrust; - 0013: “no runtime instrumentation”;
- 0014: “no runtime checks”,
analysis-incomplete, and the callback-global fixpoint (replaced by §9.3); - 0015:
analysis-incomplete; - 0016:
analysis-incompleteand--strict-externs; - 0017: “no runtime instrumentation” and
analysis-incomplete.
docs/rfcs/README.mdgets an index row for 0030, and the rows of 0018–0029 change to Superseded.
Repository documentation:
README.md: the quick look first; one guarantee statement (this RFC’s, shortened); the modes; no per-RFC changelog.docs/pages/reference/guarantees.md: replaced by the §Soundness guarantee, A1–A5 and the blame property.docs/annotations.md: every id with its severity, message and trigger, plus the new and removed macros.docs/pages/reference/cli.md: §16.docs/architecture.md: §1 and §14.docs/roadmap.md: milestones re-pointed at 0030, 0031 and 0032.CONTRIBUTING.md: test naming by feature, and the link todevelopment-history.mdremoved.AGENTS.md, in one set of edits:- RFC 0030 is the current model, read after RFC 0001; there is no checked mode;
- rule 6 no longer mentions
development-history.md; - rule 8: new tests are named by feature (
test/cases/<area>/…,test/Emission/<feature>-*.c), and existingrfcNNNN-names may stay; - the “Where things are” table:
LibrarySpec.txtreplacesBuiltins.cpp; new checker rules go behind the seam (LedgerAdapter,SafetyEngine); the sidecar row points toUnitRecord(format 28);run-cases.pyandcorpus-gate.pyreplacecorpus.py; the row that points atscripts/corpus/README.mdpoints attest/corpus/.
The docs site. The Starlight build (docs.yml runs npm test and
npm run build) reads several files this RFC deletes, and H2’s grep covers
its pages:
docs/astro.config.mjs: the sidebar entries Checked code, Checked builds, Checked annotations, Checked limits, Cache & performance and Validation records are removed; the site description changes.docs/scripts/prepare-content.mjs: thechecked-code.mdsplit (including Checked selection (RFC 0018)), theincremental-analysis,development-historyandvalidation-rfc*routes are removed; the “Checked guarantees” link text becomes “Guarantees”, and the checked sentence in the diagnostics index goes.docs/scripts/content.test.mjs: its route expectations follow.docs/data/diagnostic-remedies.json: the four removed ids go and the five added ids get remedies, since the build fails on an id without one.docs/scripts/check-examples.pyanddocs/examples/checked.c: the checked example is replaced by a trap-and-ledger example.- Pages:
docs/src/components/Home.astro,docs/pages/index.mdx,docs/public/social-card.svg(tagline),getting-started/{introduction,first-check}.md,guides/{adoption,build-integration,whole-program,unsafe,ownership}.mdandreference/{troubleshooting,compatibility}.mddrop checked mode,--strict-externs, caching and “suppresses ordinary reporting”, and describe the checks, the ledger and the require levels.
Release tooling. scripts/package-source.py drops
docs/development-history.md from its required-file list, and
scripts/test-release.py drops its fixtures and assertions for it. Both
land in S0, with the deletion, and scripts/test-release.py passes in the
S0 gate.
CI. In S8:
.github/workflows/ci.yml: the Recall and Pinned corpus steps (recall.py,corpus.pywithscripts/corpus/rfc0014.json) are replaced by thecases-*CTest entries andcorpus-gate.py --quick; CTest runs with-j; the population entries are gone. The pinned-corpus step is rewired in S0, in the same change that deletesrfc0014.json..github/workflows/corpus.yml: runscorpus-gate.py --fullon the pinned SHAs instead ofcorpus.pywithbaseline.json.check-hygiene.pyruns in the Linux Release job.
20. Performance expectations
Section titled “20. Performance expectations”The limits are gates G14, G15 and H1; this section gives only the reasoning.
- Compile. Deleting the checked hooks,
InputIdentity’s second preprocessing, and checked sidecar encoding outweighs the new passes.AttributeReader,KindInferenceandSiteCollectorare linear AST and CFG walks; slot solving is near-linear; Houdini re-runs only functions that touch a dropped candidate’s fields. The deferred CodeGen only reorders work. The new cost is the generic reporting pass for functions that today report only through call contexts (§15 item 18), which the shared context budget of §5.5 offsets. - Runtime. Null checks cost 1.02x on Lua (measured with
-fsanitize=null, which checks every dereference). Checks against named extents behave like-fsanitize=array-bounds(0.99x measured), not likelocal-bounds(1.44x), and the optimiser removes the ones a loop bound already implies. The zero-init cost is proportional to the slack between requested and usable sizes, sincecallocsupplies the rest. - CI. Removing the 243 populations and running CTest in parallel is what brings the CTest steps under H1.
Annotation surface
Section titled “Annotation surface”resources/include/weavec.h changes, mirrored in
include/weavec/Analysis/Annotations.h (spelling::) and
docs/annotations.md. WEAVEC_H_VERSION_MINOR becomes 9.
Added. Like every WeaveC macro, each expands to
__attribute__((annotate(...))) where annotate exists, and to nothing
elsewhere (including GCC):
/* On a pointer parameter or field: at least `n` elements (bytes for void * and character pointees) are accessible; `n` names a sibling parameter or * field, in any position. */#define WEAVEC_COUNTED_BY(n) WEAVEC_ANNOTATE_("weavec.counted_by." #n)/* On a pointer parameter or field: [p, q) lies in one object; `q` names a * sibling pointer parameter or field. */#define WEAVEC_ENDED_BY(q) WEAVEC_ANNOTATE_("weavec.ended_by." #q)/* On a pointer parameter, field or return type: NUL-terminated within its object. */#define WEAVEC_STRING WEAVEC_ANNOTATE_("weavec.string")/* Before a function definition: its sites are held to -fweavec-require=checked. */#define WEAVEC_REQUIRE_SAFE WEAVEC_ANNOTATE_("weavec.require_safe")These declare kinds (§7.2). They are authoritative for callers, whose call sites check them. Inside the definition they hold under A1, and the link step verifies them against every declaration.
Removed: WEAVEC_CHECKED. Code that uses it no longer compiles; replace it
with WEAVEC_REQUIRE_SAFE.
Changed meaning:
WEAVEC_UNSAFE: trusted raw spatial and null operations, and trusted raw pointers. No checks are emitted, temporal state is still tracked, possible temporal findings are still warnings, and definite violations remain errors (§6.1). Nothing inside a region is suppressed any more.WEAVEC_ASSUME(e): proven, a checked assertion, or acontradicted-assumptionerror (§6.2). The macro’s definition is unchanged and the check is inserted byweavec-cc; its doc comment, which says it is “trusted like every annotation”, is rewritten.WEAVEC_NONNULLon a parameter: a possibly-null argument is now checked at the call instead of reported. A definitely-null argument is still an error.WEAVEC_SIZED_BY(n): unchanged in spelling and in units (elements; bytes forvoid *), which makes it a synonym ofWEAVEC_COUNTED_BY, and unlike Clang’s byte-countingsized_by. It now also produces checks: at call sites, and at accesses through the annotated pointer when not proven. Its doc comment says so and recommendsWEAVEC_COUNTED_BYfor new code.
Unchanged: WEAVEC_OWNED, WEAVEC_BORROWED, WEAVEC_MUT, WEAVEC_RAW,
WEAVEC_NULLABLE, WEAVEC_RETAINS, WEAVEC_RELEASES, WEAVEC_REFCOUNT,
WEAVEC_OWNED_BY and WEAVEC_ENABLED.
The ecosystem attributes in the §7.2 table are read, but WeaveC defines no macros for them.
Diagnostics
Section titled “Diagnostics”After this RFC, weavec::core::diag has 20 ids: the 15 kept below and the
5 added, which are exactly the ids the owner’s resolutions list. “Severity” means: an
error when definite, a warning when possible, unless the table says
otherwise. diag::isWarningByDefault is replaced by
diag::defaultSeverity(id, certainty), and diag::isEnabledByDefault(id)
is false only for allocation-failure.
Kept ids.
| Id | Severity | Messages (changes in bold) |
|---|---|---|
use-after-free |
definite error / possible warning | use of '<p>' after it was freed; use of '<p>' after it may have been freed (note freed here on some paths); the reference forms … after its reference was released / … may have been released |
double-free |
definite error / possible warning | '<p>' is freed twice; '<p>' may be freed twice (note previously freed here on some paths); … released twice forms likewise |
use-after-move |
definite error / possible warning | use of '<p>' after it was moved; use of '<p>' after it may have been moved |
conflicting-borrow |
definite error / possible warning (§3.1) | unchanged messages |
lifetime-too-short |
definite error / possible warning (§3.4) | unchanged; also for returning alloca storage |
unsafe-operation |
error | unchanged (RFC 0004): an access through a pointer with a raw origin (declared WEAVEC_RAW, converted from an integer, loaded through a raw pointer, handed out as raw) outside an unsafe region. The --strict-externs forms (unchecked call to '<f>' outside an unsafe region, unchecked call through '<fp>' …) are removed with the flag |
annotation-mismatch |
error | unchanged; it now covers declared and relied-upon interface facts. New at link: '<f>' is declared <annotation> here but its definition in '<file>' <frees/stores/…> '<param>' (note defined here); store to '<S>.<field>' breaks '<invariant>', which '<unit>' relies on |
invalid-annotation |
warning | unchanged; new: '<n>' in WEAVEC_COUNTED_BY does not name a parameter or field, conflicting kinds for '<p>': <k1> and <k2> |
leak |
warning, never error | unchanged; not reported at a return from main or after an exits call |
mismatched-release |
definite error / possible warning | unchanged |
null-dereference |
error, definite only | dereference of '<p>', which is null; '<p>', which is null, is passed to '<f>', which dereferences it. The “may be null” forms are removed: they are checked facets now |
use-of-uninitialized |
error, definite only | use of '<p>' before it was initialized. Possible uses are made defined by zero-initialisation |
invalid-release |
definite error / possible warning | unchanged; possible: '<p>' is released but may point to <x>, which is not a heap object, and for an interior pointer at an unknown offset '<p>' is released but may not point to the start of its allocation |
out-of-bounds |
error, definite only (an exact extent, §7.1) | existing definite forms; new: '<f>' writes <n> bytes into '<b>', an object of <m> bytes; write through '<p>', which points to a string literal; format string of '<f>' reads <n> arguments but <m> are passed; '<a>' has <n> elements but '<f>' accesses <m> through parameter '<p>' (a static-callee requirement at the call); '<f>' copies <n> bytes between overlapping ranges of '<obj>' (note '<obj>' is declared here). The “may” forms are removed: they are checked facets now |
invalid-integer-operation |
error | unchanged (RFC 0017) |
Added ids. Each gets a unit test, a lit test pinning the message, and a
row in docs/annotations.md.
| Id | Severity | Message and notes | Minimal trigger |
|---|---|---|---|
contradicted-assumption |
error | assumption '<e>' is false here; note '<x>' is <value> here at the fact’s source |
char b[4] = {0}; int i = 10; WEAVEC_ASSUME(i < 4); return b[i]; |
allocation-failure |
warning; off by default (-Wweavec-allocation-failure) |
the result of '<f>' is used without a null test; it is null when allocation fails; note allocated here |
char *p = malloc(8); p[0] = 1; |
unresolved-operation |
error; only under -fweavec-require=checked|proven or WEAVEC_REQUIRE_SAFE |
<operation> is neither proven nor checkable: <reason phrase> [<reason>], for example access 'b[1000]' is neither proven nor checkable: the extent of 'b' is unknown [unknown-extent] |
char *get(void); int f(void) { return get()[1000]; } with -fweavec-require=checked |
unchecked-operation |
error; only under -fweavec-require=proven |
<operation> relies on a runtime <template> check; note naming what the proof lacked (nothing is known about the nullness of 'p') |
int f(int *p) { return *p; } with -fweavec-require=proven |
unanalyzed-input |
warning, at link, once per link | link input '<path>' has no WeaveC record; calls into it are trusted, or link input '<path>' has a stale WeaveC record (<why>); calls into it are trusted; with several inputs, <n> link inputs have no WeaveC record; calls into them are trusted with one note per input |
weavec-cc -c a.c; clang -c b.c; weavec-cc a.o b.o |
A definite overlap of a copy’s ranges is reported as out-of-bounds
(§3.3) rather than under an id of its own, so the added ids are exactly
the five the owner’s resolutions list.
<operation> is one of access '<text>', dereference of '<p>',
call to '<f>', release of '<p>', conversion of '<p>' to '<T>' or
boundary of '<fn>'. The reason phrases are:
| Reason | Phrase |
|---|---|
unknown-extent |
the extent of '<p>' is unknown |
unknown-index |
the position of '<p>' in its object is unknown |
inexpressible |
its bound has no name here |
may-released |
'<p>' may have been freed |
may-moved |
'<p>' may have been moved |
may-alias-released |
'<p>' may point into an object freed earlier |
may-invalid-release |
'<p>' may not point to the start of a heap object |
may-mismatched-release |
'<p>' may belong to another allocator |
may-dangle |
'<p>' may outlive its storage |
may-conflict |
'<p>' may still be borrowed |
unknown-callee |
'<f>' may have freed or kept '<p>' |
callback |
the target of '<slot>' is unknown |
setjmp |
'<fn>' calls setjmp |
budget |
'<fn>' exceeded the analysis budget |
unanalysed |
WeaveC does not model this (<detail>) |
raw-cast |
'<p>' was made from a non-pointer value |
dangling-escape |
'<place>' may hold a freed pointer here |
second-owner |
'<a>' and '<b>' may own the same object here |
no-zero-init |
'<p>' may be uninitialised |
Removed ids:
analysis-incompletebecomesunresolved(unanalysed | budget | …)ledger rows plus the summary line;annotation-requiredbecomesunresolved(unknown-callee)rows with fix-its;checking-incompleteandchecking-failedgo with checked mode.
A -W flag naming a removed id is an error,
unknown WeaveC diagnostic '<id>' (removed by RFC 0030), like any other
unknown id. No compatibility aliases are kept.
dangling-escape and second-owner are ledger reasons, not ids. They
surface as unresolved-operation errors only under require levels.
Implementation plan
Section titled “Implementation plan”All stages land on branch rfc0030-prove-or-trap and ship as one commit,
as the owner directed. Each stage ends with its gate green in a local run.
The PR description records each result: the command, the binary and the
numbers. Two fallback points and one abort rule bound the risk.
| Stage | Work | Gate to leave the stage |
|---|---|---|
| S0 Oracles and reset | This RFC. The golden v0.10.0 binaries (§17.1). test/cases with every import, the main drivers of §17.2 and the proofs/ salvage. scripts/run-cases.py including --legacy. test/corpus/ (manifest with pinned SHAs, expected, triage, about 30 injections, bench, support files) and scripts/corpus-gate.py. Deletion of the generated scripts/corpus/rfc00NN-* data, the validation records, development-history.md and the README changelog prose, with the release-tooling and pinned-corpus CI changes of §19. |
run-cases.py --legacy with the golden binaries reproduces v0.10.0: evaluation 44/44 bugs and 32/32 clean; RFC 0017 cases 24/24; recall 67/67; probes 36 caught / 42 silent / 4 signal / 1 leak-only / 2 mislabel; engine pins 100%. corpus-gate.py --quick --legacy reproduces the 301 bug claims. --inject --legacy records the injection baseline, matching the dossier where it measured (jansson 3/3, cJSON 1/1, sds 2/2, linenoise 1/1, Lua 0/2). scripts/test-release.py passes. |
| S1 Retire checked mode by reachability | Remove the checked-mode and analysis-cache options; then the guarded branches (§15 item 1); then everything unreferenced (§18); then the populations, runners, reports, cache, InputIdentity, and the checked unit and lit tests. Make CTest parallel. --strict-externs, --exclusive-borrows, --report-unannotated and --analyze-headers stay until S3, so the output can stay identical. |
Sorted ordinary diagnostics (file, line, column, id, message) are byte-identical to the golden run on all of test/cases and on the 11 configs (--compare-golden in both scripts). Unit and lit tests pass. |
| S2 Deferred CodeGen | DeferredCodeGenConsumer (§10.5), with no rewrites. scripts/codegen-identity.py and test/corpus/identity.txt. |
G7. |
| S3 Ledger and semantics | Core Ledger, AttributeReader and the syntactic kinds that SiteCollector needs, SiteCollector, the seam (§14) with beginFunction and the authoritative pass (§2.6, §15 item 18), DataflowEngine, certainty and the temporal rules (§3, §15 items 16–17), the object extents (§7.4, §15 item 15), sound defaults (§5.1; §5.2 with the header list, which is the first part of LibrarySpec.txt to land; §5.4–5.7), unsafe/assume/require (§6, errors only), CheckPlanner::plan (pure planning, no emission), budgets, LedgerWriter (§12), the -p and SARIF fixes, removal of analysis-incomplete, annotation-required and the four flags S1 kept, and the lit and unit test updates of §17.6. |
Under run-cases.py --no-emission: G1; G2 (every pin at any severity); G3. G4 for the probes whose mechanism exists by S3, that is all but 02, 02c, 02d (S7), 09, 14c (S6), 15, 15b, 25, 25c, 47 (S4), 38 (S8), and 08, 32 (S5, counted as MISS): 0 silent, 0 matching facet proven, and 45 and c03 are errors. zlib ./configure && make succeeds with no definite error triaged false. H3 for the ids added so far. |
Fallback point A. S0–S3 ship as “one semantics, visible coverage”: checks are not emitted, and the summary line says checkable (not enforced). The probes excluded from S3’s G4 subset are not yet covered, and the release notes say so. |
||
| S4 Library table | LibrarySpec (§8) replaces Builtins, RuntimeModels and the name == tests, with aliases and fortified forms; model fixes; callbacks, threads and signals (§5.3); allocation-failure; the leak rule; the §9.2 non-null derivation. |
A unit test per row, from the independent expectation table. The ledger diff S3→S4 on test/cases and the corpus, with every changed row explained in the PR description. G4 for probes 15, 15b, 25, 25c and 47 under --no-emission. Repros pred, zerolen and mainleak meet their exact expectations. |
| Fallback point B. S0–S4 are a coherent release if check insertion fails: one semantics, recorded outcomes, the library table, and the test reset. | ||
| S5 Emission | CheckEmitter over the plan for type-derived, allocation and declared extents; the prelude and its forms; the four modes, weavec_rt and weavec_chk; lowered-violation traps (§3.4); zero-initialisation (§11); the PCH fallback. |
G1 and G2 in their trap form; G4 in full except probes 02, 02c, 02d (S7), 09, 14c (S6) and 38 (S8); G5; G6; G8; G11; G14. |
| Abort rule. If false traps (G5, G6, G11) or overhead (G14) cannot be fixed within S5, emission is reverted. The release is then S0–S4 plus S6’s declared kinds, in the ledger only. §10–§11 move to RFC 0031, and this RFC is amended to record the cut; it does not become Implemented until that amendment. | ||
| S6 Kinds | AttributeReader’s full table and the macros (§7.2); the engine’s consumption of kinds (§15 item 14); slot kinds with reliance flags and demotions (§7.3); must-access inference with guarded call-site checks (§7.5); grouping (§7.4). Houdini (§7.6) comes last and is the first thing cut. |
A lit test per row of the §7.2 table; G13; G4 for probes 09 and 14c; G5, G11 and G14 still pass. |
| S7 Temporal precision | Case derivation with lossy bits (§9.1); slots per TU and in --whole-program (§9.3), replacing the RFC 0014 callback-global fixpoint; boundary invariants and their propagation (§9.4). |
G9, G10, G14 and G15; G12 measured with weavec --whole-program (the weavec-cc link half waits for S8); G4 for probes 02, 02c and 02d. Repros realloc0–realloc5 and luaalloc meet their exact expectations. |
| S8 Link and wrap-up | Format 28 (§13.1); the link step (§13.2), including declaration verification, reliance checks, program-wide propagation and unanalyzed-input; CLI cleanup (§16); documentation, the docs site and RFC statuses (§19); CI wiring: parallel CTest, the cases-* tests, corpus-gate --quick on PRs, --full weekly, and check-hygiene. |
Every gate below re-run on the final tree, including probe 38 at link, G12 through weavec-cc linking the objects, H1 on the PR’s own CI run, and H2. npm test && npm run build in docs/ pass. |
Amendment (S6 exit). Stage S6 lands without §7.6 (cut, see there) and
with the §7.5 and §10.3 amendments above. Store groups (§7.4 rule 7) are
decided after the group’s last store, proven or unresolved: a check after
the stores has no placement yet (unresolved(inexpressible)), and a
definite shortfall of a WEAVEC_COUNTED_BY field is not yet an error (only
RFC 0012’s WEAVEC_SIZED_BY fields report annotation-mismatch). A
declared ended-by is enforced at calls when both arguments point into one
object at known offsets, but gives the body no extent (the engine has no
term for a pointer difference). invalid-annotation for the extent macros
uses AttributeReader’s wording ('<n>' in WEAVEC_SIZED_BY does not name a parameter or field) for both synonyms; WEAVEC_NULLABLE with
WEAVEC_NONNULL keeps RFC 0008’s. The engine’s kind consumption lives in
lib/Analysis/KindSeeding.cpp, which includes Dataflow.h but is not a
Dataflow* file, to hold gate H2’s line budget.
Amendment (S3 exit). Probes 20b, 36 and 46 need the Call-site facets
of inferred requirements (§7.5), and probe 27’s bug site stays proven until
the boundary invariants of §9.4 exist. They move from S3’s G4 subset to S6
(20b, 36, 46) and S7 (27). A multi-unit case whose defect only the link step
sees (a callee defined in another unit) is a definite error at link; its
objects are already compiled, so its TRAP markers were removed rather than
expected from a lowered link error (§13.2 step 4).
Only Houdini field invariants (§7.6) can be cut without breaking the design; G13 is sized to pass without them. The report-mode runtime cannot be cut, because the case runner and G11 attribute traps through it (§17.4). The slot analysis cannot be cut either, because G12 is a hard gate. Unfinished inference always degrades to unresolved, never to proven, so a cut is sound by construction.
Acceptance gates
Section titled “Acceptance gates”Unless stated otherwise:
- the binaries are
weavec-ccandweavecfrom thereleasepreset (build/release/bin/) on the final tree; - the reference compiler is
$WEAVEC_LLVM_PREFIX/bin/clang(LLVM 23); - “reported at a line” means a diagnostic with the expected id at that line (an error or warning, per its marker), or a trap at that line with the expected template in the executable oracle.
Gates marked (changed) differ from the milestone plan; the reason is given.
Parity and recall
- G1. Measured by
scripts/run-cases.py --filter 'evaluation/**' --filter 'pairs/**'in trap mode.- The 44 evaluation bug cases have every
BUGmarker satisfied. - The 32 clean cases have no errors and no traps. Warnings are allowed
only through an
ALLOWline, and eachALLOWis justified in a comment in the case file. - All 24 RFC 0017 cases (12 bug/clean pairs) pass.
- The 44 evaluation bug cases have every
- G2. Measured by
run-cases.py --filter 'recall/**'.- Every recall pin is reported (67 as
recall.pycounts them). - Across evaluation and recall, the runner’s
errorsOrTraps / pinsis at least 0.75.
- Every recall pin is reported (67 as
- G3 (changed). Measured by
run-cases.py --filter 'engine/**'.- At least 95% of the lit engine pins converted from the golden run are reproduced at any severity.
- A pin whose golden message said a pointer “may be null” or an access “may be” out of bounds counts as reproduced when the matching facet at that line is checked. This policy deliberately turns such findings into checks.
- Every miss is listed in
test/cases/KNOWN-DIFFERENCES.md, with at most 25 entries.
- G4 (changed). Measured by
run-cases.py --filter 'soundness/**' --asan.- 0 of 85 bug probes are silent. “Non-silent” means error, warning, trap,
a non-proven row of the matching facet (§17.3), or
NEUTRALISEDfor probes 08 and 32. Those two defects are uninitialised scalars, outside every facet; zero-initialisation defines them away. - At least 55 of 85 are reported as an error, warning or trap.
- Every probe v0.10.0 reports (the 36 caught, plus 26 and 41) is still reported, at some severity.
- 0 ASan-reported bug sites have the matching facet proven. For
probes 02, 02c and 02d the bug site is inside the reader (
peek), and it is non-proven through the boundary propagation of §9.4. - Probes 45, c03 and 38 produce errors. Probe 38 is built with
38_extern_impl.cand caught at link. - With
--filter 'semantics/**' --asan, the review cases of §17.2 pass: the aliasing, reliance, propagation and concurrency cases have no proven matching facet at their ASan-reported bug lines.
- 0 of 85 bug probes are silent. “Non-silent” means error, warning, trap,
a non-proven row of the matching facet (§17.3), or
- G5. Measured on the 28 correct twins.
- All 28 build with no errors and run their
RUN-INPUTs with no trap. - With
FLAGS: -fweavec-require=checkedadded by the runner’s--require checked, at least 20 of 28 build with no errors (11 today in checked mode).
- All 28 build with no errors and run their
- G6 (changed). Measured by
run-cases.py --checks verifyover every executable case, andcorpus-gate.py --full --checks verifyover every project test suite.- 0
weavec.proventraps. - Every
proofs/case satisfies itsNOT-PROVENmarkers. - Every proven Index facet whose extent is exact and expressible receives a verify check, and the ledger reports the verify coverage of all proven spatial and null facets. The change: without a coverage requirement, verify mode could monitor only null proofs, while the known false proof (the reader cursor) was spatial.
- 0
Codegen
- G7. Measured by
scripts/codegen-identity.py --list test/corpus/identity.txt(at least 100 corpus TUs).- For each TU, in each of the configurations
-O2,-O0 -gand-O2 -flto=thin, the object fromweavec-cc -fweavec-checks=none -cis byte-identical (cmp) to the one fromclang -c -D__WEAVEC__=1 -isystem resources/include. - The gate is at least 100 TUs × 3 configurations.
- For each TU, in each of the configurations
- G8. Measured by
lit test/Emission.- At least 30
rewrite-oracle-*.cpairs. - In each pair, the
-O0IR of the instrumented source equals, after--strip-debugnormalisation, the IR clang produces for the paired hand-written*.expected.c, which calls the same helpers explicitly and is compiled with-includeof the prelude thatweavec-cc -fweavec-print-preludeprints. - The pairs include the zero-length
nonnullform, thespanindex form, thesprintflowering, a lowered violation, and thememcpy(NULL, x, 0)fold case of §8.3.
- At least 30
Real code. All measured by scripts/corpus-gate.py --full, with
weavec-cc in its default mode (trap, zero-init) on the pinned SHAs of
test/corpus/manifest.json.
-
G9 (changed).
- All 11 configs build, including zlib’s own
./configure && make. - At most 10 definite errors in total, each with a
triage.jsonentry whose verdict istrue. Afalsedefinite error fails the gate. - A
buildconfig reaches a successful build despite a triaged-true definite error only through the per-config-Wno-error=weavec-<id>recorded next to its triage entry (§17.5); in per-filecompileconfigs the error is recorded against its TU. The change: a definite error drops its object, so “all build” and “up to 10 true definite errors” contradicted each other without this rule. - zlib
minigzip’s repeatedfclose(stdout)is still reported.
- All 11 configs build, including zlib’s own
-
G10. Possible temporal warnings number at most 60 in total, all triaged. jansson has at most 20 (84 today, as errors).
Measurement (S7). 671 today, of which 623 are Lua’s and 619 are one family: the string table’s vector and
luaO_pushvfstring’s buffer, both reallocated through the allocator slot and left in place when the reallocation returns null. Three things about it are measured rather than supposed, and each supersedes an earlier guess.First, it is not waiting on a field invariant. Supplying
luaS_resizewithnsize != 0directly — the fact astrt.size >= 2invariant of §7.6 would give, at the point of use and with no inference chain to lose it — leaves the count at 617 possible double-frees, unchanged. A lower-bound instance of §7.6 would also not have survived its own Houdini:tb->size = nsizestores a parameter, and the shrinking call passesg->strt.size / 2, which refutes every constant lower bound.Second, this subsection’s derivation did lose the guard, and the loss is now fixed. A wrapper that retries —
p = alloc(n); if (!p && n > 0) p = alloc(n);, which isluaM_realloc_’s shape — returns a merge of two call results, so thePendingOutcomeof each reaches the merge at the end of theif. The retry’s null class consumes nothing, becausenotePendingOutcomedrops a consume whose guard the arguments refute andn > 0refuteswhen[n = 0];PendingOutcome::uniteread that silence as disagreement and erased the outcome, so the wrapper exported a bare unguarded consume and every caller lostreplaced. A class one side consumes nothing on now leaves the other side’s guarded consume standing (the guard is why that side is silent, and §3.1 keeps a guarded consume from settling into a certainty).luaM_realloc_exportsoutcome null{block: freed(free) when[nsize = 0]}where it exportedblock: freed(free)with nowhenand no classes at all.test/Analysis/rfc0030-reallocator-outcomes.creduces this to three functions one line apart and pins the true positive beside the two clean ones.Third, that did not move the count, because the family has a second and independent cause: the allocator frees the string table itself.
tryagainruns an emergencyluaC_fullgc, which re-entersluaS_resize, soluaM_realloc_’s own summary carriesL->l_G->strt.hash: freed(free) when[nsize in u64:1-…]— the complement of thensize == 0condition under which it freesblock. InluaS_resizethe two consumes of that one path join to a trivial guard, the0-0conjunct goes, andreplacedwith it: the recursion is cut before thetb->hash = newvecton the other side of it can come back. This is why the size fact buys everything on the reduction and nothing on Lua — on Lua the conjunct it would refute is already gone. RemovingluaC_fullgcfromtryagaintakesstrt.hashfrom 606 to 234, but the total from 623 to 1090, since allocation failure then reaches callers that never expect it: it is evidence for the mechanism, not a clean A/B.So the residual is a
replacedfact that does not survive a recursive call, not a widening and not a missing invariant. The budget is left at 60 rather than widened per project.The family is deliberately not enumerated in
triage.json. Every other finding in the corpus carries an entry with the source that settles its verdict; these 623 share one cause, and 623 near-identical entries would bury the 48 that each say something. The gate therefore reports them as untriaged, which is accurate: they are accounted for here, not dismissed. Enumerating them becomes worthwhile only if triage learns to record a family once, which is worth doing before the next corpus grows one. -
G11. 0 traps in the project test suites: cJSON’s tests, jansson’s test suite, zlib’s
make test(exampleandminigzip), the Luatestessubset named in the manifest, sds’ssds-testand jsmn’s tests. A trap is death bySIGTRAPorSIGILL, or aruntime check failedline in the report-mode rerun. A trap at the site of a triaged-true definite error (G9) is a true positive and does not count. -
G12 (changed). Measured by
corpus-gate.py --full --inject.- At least 90% of the injections are reported at the injected line.
- Both Lua allocator injections are reported in whole-program mode: by
weavec --whole-programon the lua config, and byweavec-cclinking the objects directly. The change: “caught” is made precise as “reported at the injected line”.
-
G13. Measured by
corpus-gate.py --quick. The unresolved share is unresolved spatial and null facets divided by all spatial and null facets.- linenoise.c at most 0.25 and cJSON.c at most 0.25 (target 0.15); sds.c at most 0.60. Today these are 0.86, 0.80 and 0.99, all silent.
- The values are committed in
expected.jsonas a ratchet.
Cost
-
G14. Measured by
corpus-gate.py --bench: the minimum user CPU time of 7 runs of theweavec-ccdefault build, divided by the same for aclang -O2build, on the same machine. The limits are Lua bench (bench/lua-bench.lua) ≤ 1.10, zlibminigzipcompress+decompress of the generated 64 MiB input ≤ 1.10, and cJSON parse+print (bench/cjson-bench.c) ≤ 1.15.Measurement (S7). zlib 1.0162 and cJSON 1.1425 pass. Lua is 1.4783 and fails, and the limit is not moved, because the number decomposes into one part that meets it and one that has nothing to do with checking.
With
-fweavec-checks=nonethe build is 0.99x and zero-initialisation costs 0.03 s, so the checks are the whole of it; all but 0.07 s of the 0.40 s lands in the interpreter’s dispatch loop. Its 432nonnulland 122indexchecks cost 0.01 s and 0.07 s when either family is inserted alone, but 0.33 s together. The super-linearity is a code-layout cliff, not a cost of checking: LLVM’s post-register-allocation tail duplicator copies the block ending in theindirectbronly into predecessors that have a single successor, and with both families presentSimplifyCFGhas folded away each check’s empty continuation, so 133 of the 161 edges into the dispatch are a check’s conditional branch. The dispatch is replicated into 14 predecessors whereclang -O2reaches 91 and a-fsanitize=null,array-boundsbuild of the same source reaches 79 and costs 1.05x. With either family alone the continuation survives and duplication works.Splitting those 66 critical edges in the IR and changing nothing else gives 80 duplicates and 1.19x, with byte-identical benchmark output; on clang’s own pipeline the checks then cost 0.08 s, which is 1.10, exactly this gate’s limit and consistent with §20’s reasoning. So checking Lua costs what was predicted, and 0.26 s of the 0.41 s is the layout cliff. A reproducer is kept: the same module gives 14 duplicates before the split and 80 after.
The fix belongs on the emission path, which §10 owns, so it is left to its own amendment and change rather than taken here: a pass at
OptimizerLastsplitting critical edges into any block that ends in anindirectbrand has many predecessors. It is shape-keyed, names nothing, and is a no-op on ten of the eleven corpus configurations, since only one of them uses computed goto; where it fires it costs about 4.6 KB of code. Two things must be settled with it: this LLVM does not installllvm/Passes/PassPlugin.h, which is a packaging gap and not a reason to hand-roll the plugin ABI, and G7 must be re-measured rather than assumed safe, although a plugin passed only when checks are inserted cannot reach a-fweavec-checks=nonejob. -
G15 (changed).
- Lua whole-program analysis (
weavec --whole-programon the lua config) takes at most 214 s CPU. - zlib’s
make -j8withCC=weavec-cctakes at most 5.4 s wall (the golden binary measured 5.38 s; serial, 6.59 s). - On a machine other than the reference one, the limit is instead the golden binary’s time measured in the same run. This is the change: a fixed time is only comparable on one machine.
- Over-budget functions are at most 1% of analysed functions, and each is listed.
Measurement (S7). Two of the four clauses pass: over-budget functions are 0 of 3,872, and zlib’s
make -j8is 2.06 s against 5.4 s. The Lua time fails, at 307 s against 214 s, and 2.02x the golden binary measured back to back on an idle machine (152 s). The limit is not moved.§20 expected the deleted checked hooks and the second preprocessing to outweigh the new passes. They did not, and the reason is not a hot loop:
--analysis-statsputs 54.5% of the dataflow time in the interpreter’s dispatch function alone, and everything else in Lua together costs less than the golden’s whole run. The golden computed none of the facets, kinds, slots, boundary rows or records this RFC adds, so the comparison is between two different amounts of work, but that does not excuse the figure: it means the estimate in §20 was never derived from the analysis this RFC specifies.What was tried and rejected matters as much as the number. A reverse post-order block worklist reached 54 s, but only by exhausting that one function’s §5.5 budget in 8 runs out of 11 and losing 61 findings, so it was reverted: this gate may not be met by analysing less. Interning
SummaryPathis the remaining identified lever, worth about 10% of a 2x gap, since about 19% of the time is in allocation. Reducing the rounds honestly is the real one, and it is the same work as G10’s residual — both are about facts that do not survive a cycle. - Lua whole-program analysis (
Hygiene
- H1 (changed). Measured at S8 on the PR’s own CI run.
- In the Linux Release job,
ctest --preset ci-release -j$(nproc)runs in at most 120 s wall. - In the Linux ASan/UBSan job (
ci-debug), the CTest step runs in at most 8 minutes, with the cases suites in--no-runmode. - The change: the milestone plan capped the whole ASan job at 20 minutes, but installing LLVM and building under ASan already take about 22 minutes of today’s 41 (CTest is 19), so no test change can meet it. The CTest step is the part this RFC controls.
- In the Linux Release job,
- H2 (changed). Measured by
scripts/check-hygiene.py:- 0 occurrences of
SafetyState,CheckedContract,checkContractsor--checkedinlib/,tools/anddocs/outsidedocs/rfcs/. The change: superseded RFCs are the historical record and keep their text. - 0
== "<name>"comparisons against aLibrarySpecentry name outsidelib/Core/LibrarySpec*. - 0 occurrences of the words
cJSON,jansson,linenoise,jsmn,zlib,lua/Lua,sdsorminigzipanywhere inlib/, comments included. - No new component (§1) includes
Dataflow.h, andFunctionDataflowreceives noDiagnosticSink. Dataflow*.{h,cpp}total at most 26,500 lines.- The code under
lib/,include/andtools/totals at most 88,000 lines,lib/Core/LibrarySpec.txtexcluded. - Both limits were raised from 25,000 and 62,000 by the S7 amendment in §18, which records the measurement behind them. They are ratchets: lowered as code is deleted, raised only by a further amendment that measures again.
- 0 occurrences of
- H3 (changed).
test/Driver/compilation-database-p.candtest/Driver/diagnostics-format-sarif.cexist and pass.- Each added id (
contradicted-assumption,allocation-failure,unresolved-operation,unchecked-operation,unanalyzed-input) has adocs/annotations.mdrow, adocs/data/diagnostic-remedies.jsonentry, a unit test and a lit test pinning its message. - The change:
dangling-escapeandsecond-ownerare reasons, not ids, per the owner’s resolution, and the genericunresolvedid is namedunresolved-operation.
Drawbacks
Section titled “Drawbacks”- Binaries can now abort. A correct program that relied on undefined
behaviour that happened to work (§Soundness) will trap in production.
-fweavec-checks=reportsupports canary rollouts, without a guarantee while it lets failed checks continue, and-fweavec-checks=nonerestores Clang’s exact output. Both are opt-outs a team must know about. - Proven outcomes are only as sound as
FunctionDataflow. That engine has had false proofs (the reader-cursor case). A wrong proof now removes a check instead of hiding a warning. Mitigations: verify mode in CI (G6), the ASan oracle (G4), the salvagedproofs/cases, and sound defaults landing before emission (S3 before S5). - The temporal guarantee is thinner than the spatial one. Temporal facets are proven or listed, never enforced. On complex code such as Lua, the unresolved temporal count will be large until RFC 0031. This RFC commits only to “never silent” for temporal safety.
- Possible temporal bugs no longer fail the build. This is the price
of removing about 92% false errors.
-fweavec-require=checkedrestores fail-closed behaviour per unit or per function. - Per-TU compiles see more
unknown-calleerows. Calls into sibling units are unknown until the link step, which refines them. Archives still hide their members until RFC 0032, though the link step now says so. - Zero-initialisation has a cost. It costs
callocinstead ofmalloc, plus time proportional to the slack between requested and usable sizes, and it changes two observable behaviours: comparing a function pointer withmalloc, andrealloc(p, 0), which returns a one-byte block instead of freeing. - Mutating the AST in a release-mode Clang is delicate. Brew’s LLVM 23 has assertions off, so a malformed rewrite could miscompile silently. Mitigations: only six templates, all built by Sema’s own builders; a rejected rewrite restores the subtree and fails the compile rather than emitting a partial one; the rewrite oracle (G8); strict warning builds of instrumented code; the identity gate (G7).
- The commit is large. About 32K authored lines and whole-file deletions of about 380K, most of them generated. The stage gates and the two fallback points exist because of this.
- Checked mode’s 150 complete contracts are lost as a notion. Their
functions’ spatial and null operations become proven or checked, and
-fweavec-require=provengives the same fail-closed stance, but the contract reports are gone.
Alternatives
Section titled “Alternatives”- Replace the engine first (a symbolic-heap “Weave Engine” with two
reporting policies). This is the best end-state architecture (library
code of about 36K lines), but parity has to be rebuilt from zero across
research-grade pieces (list segments, abduction, witness rules, Houdini).
It is 3.5 times the largest library change the project has landed, and
even a successful rewrite certifies nothing about unproven code. It
becomes RFC 0031, rescoped to replacing the
SafetyEnginebehind §14’s seam and sized by the unresolved-reason histogram. - A manifest-first bug finder (Pulse-style, under-approximate). It
gives the most trustworthy findings, but it gives up the README’s
“proves” promise, keeps
WEAVEC_UNSAFEas a suppressor, and would need a second engine for any later proof. Its slot analysis, repros, injection harness, triage gate and project-name grep are grafted here. - Adoption first (object-embedded records, baselines,
weavec.toml, SARIF). It polishes an analyzer whose reports are about 92% false, and baselines keyed to today’s messages would churn. Its certainty mechanism, fingerprints, attribute precedence,-pfix and missing-metadata warning are grafted here; embedding goes to RFC 0032. - Consolidate in place (symbolic values and a zone domain inside
FunctionDataflow, keeping proof mode). It migrates about 600 sites inside the largest class and keeps proof-cost gates that repeat RFC 0029’s blocker. Its contract/evidence split and false-proof salvage are grafted here. - Keep checked mode behind a flag. That keeps about 18K lines, the
243 populations and 82% of CI time for a mode that proves 9% of the
corpus and cannot run on Lua.
-fweavec-require=provencovers its fail-closed use. - Use Clang’s sanitizers or
-fbounds-safetyfor the checks. Neither can carry WeaveC’s inferred extents.counted_byon parameters is rejected even with-fexperimental-bounds-safety, sanitizer pruning is per function, andlocal-boundscosts 1.44x on Lua. Emitting WeaveC’s own six templates keeps the cost array-bounds-like. - Instrument at the IR level. Keying an IR pass by source location would need debug locations in every build and IR-level reconstruction of C values. Sema rewrites keep C semantics and type-check the result.
- Fat pointers or another ABI change (CCured SEQ/WILD, SoftBound, CHERI, Fil-C). These would enforce much more, including temporal safety in some designs, but they break the drop-in compiler and linking with uninstrumented code, which is the README’s first promise.
- Do nothing. WeaveC stays noisy (zlib does not build), silently unsound (42 of 85 probes) and slow in its only fail-closed mode.
Prior art
Section titled “Prior art”- CCured (Necula, Condit et al., TOPLAS 2005). It infers pointer kinds (SAFE, SEQ, WILD), proves what it can and inserts runtime checks for the rest. The Single/Counted/Unknown split and “checks for what cannot be proven” come from there. CCured’s fat SEQ/WILD pointers changed the ABI; WeaveC takes only the kinds and names extents that already exist in the program.
- Checked C and 3C. Its checked pointer types (
_Ptr,_Array_ptrwith bounds,_Nt_array_ptr), dynamic bounds checks and the rule that allocations holding checked pointers start valid-or-null inform §7 and §11. 3C’s inference of kinds from uses parallels §7.3–7.5. - Clang
-fbounds-safety. Its lattice (__single,__counted_by,__sized_by,__ended_by,__null_terminated,__indexable), its checks at conversions from indexable to single, its grouping rule for pointer/count updates, and its reliance on the optimiser (ConstraintElimination) to remove redundant checks are adopted almost directly, including the one-past-the-end rule. WeaveC differs in inferring kinds for unannotated code and in not changing pointer representation. - Rust. Ownership and borrowing are checked statically, and bounds are checked dynamically unless the optimiser removes them. This RFC gives C the same split: temporal safety is static; spatial and null safety are proven or checked.
- Frama-C RTE and E-ACSL. Frama-C generates a runtime-error assertion per operation, proves what it can and compiles the rest into runtime checks. The per-site ledger is the same idea, with outcomes recorded instead of proof obligations handed to a prover.
- Infer Pulse (Le et al., OOPSLA 2022). Only manifest bugs are reported, per-procedure budgets keep imprecision local, and disjuncts are bounded. This informs the definite-only error policy (§3) and the budgets (§5.5).
- Houdini (Flanagan and Leino, FME 2001). It is the fixpoint used for field invariants (§7.6).
- Field-based function-pointer analysis (Andersen-style inclusion constraints over fields). This is the basis of §9.3; field-based rather than object-based analysis trades precision for linear-size constraints that cross translation units.
- SoftBound/CETS, Fil-C and CHERI. These are runtime designs that enforce temporal safety too, at a cost in ABI or performance. RFC 0031’s temporal backstop starts from them.
-ftrivial-auto-var-init=zero. Deployed in Chrome, Windows and the Linux kernel, it shows that zero-initialisation is affordable and removes a bug class. WeaveC extends it to the malloc family (§11).- Clang’s lifetime-safety analysis (
-Wlifetime-safety). It covers simple dangling cases in C upstream. WeaveC’s niche is the interprocedural, ownership-aware remainder plus enforcement through a drop-in compiler.
Unresolved questions
Section titled “Unresolved questions”These can be settled during implementation without changing a decision above:
- The budget default. S3 calibrates it by the §5.5 rule. The value is recorded in the CLI reference and in a one-line correction to this RFC.
- Whether zero-initialisation meets G14 on allocation-heavy code. If
it does not, the allowed narrowing is to zero only allocations whose
size expression is
sizeofof a type containing pointers, plusrealloctails. Any narrowing must keep the §11 guarantee for pointer loads and must be written into this RFC. - Open slots with known targets. Is
trusted(extern-contract)the right outcome, or should it beunresolved(callback)? It is trusted here so that jansson’s allocator hooks keep their precision. If G10 passes either way, the stricter outcome is preferred. - The volume of
unresolved(may-alias-released). The rule of §3.1 is deliberately coarse (type compatibility, owning places, place identity). If it floods the temporal counts on the corpus, the allowed remedy is a sharper distinctness proof, never a weaker rule. - The volume of
unresolved(unknown-callee)in per-TU builds of multi-unit projects. The summary line may need to separate “resolved at link” from “unresolved at link” to stay readable. - The SARIF size for large programs. Lua has tens of thousands of
rows. If consumers struggle, unresolved rows may move to a
-fweavec-ledger-sarif=diagnosticsmode; the JSON ledger stays complete. - The fingerprint root. The nearest
.git, else the working directory, may be wrong for monorepos. RFC 0032’sweavec.tomlis the intended fix, and the fingerprint’sweavec-fp/1tag allows a v2. - User sanitizers. Combining checks with user
-fsanitize=addressshould work, because the checks are plain C. G4’s oracle builds with-fno-weaveconly to keep the two signals separate. This is to be confirmed by one lit test. - Real corpus numbers. How many possible temporal warnings remain on Lua after slot resolution, and how many header-struct invariants are relied on, will only be known from the corpus. G10 and the A3 line report them.
Future work
Section titled “Future work”- RFC 0031: residual enforcement and precision. It is sized by the
ledger’s unresolved-reason histogram and covers:
- a replacement engine behind
SafetyEngine(§14): semantic IR, symbolic heap, relational domain; - a temporal runtime backstop (a quarantine allocator; Arm MTE and Apple MIE where the hardware allows);
- an ABI-compatible heap-bounds lookup (a low-fat allocator) for the
residual
unknown-extentsites, such as the sds header idiom andcontainer_of; - precision where the histogram shows it pays: state machines, guard functions, array cells;
- proof-dependency tracking, for a sharper blame property;
- if S5 is aborted, the check emission of §10–§11.
- a replacement engine behind
- RFC 0032: adoption. It covers:
- format-28 records embedded in object sections (ELF
.weavec, Mach-O__DWARF,__weavec), so archives, shared libraries, ccache and LTO carry them; - fingerprinted baselines and reasoned suppressions;
weavec.tomlwith path scoping and API overlays;weavec suggest --applyfor the ledger’s fix-its;- a vendored
weavec.h; - relocatable installs.
- format-28 records embedded in object sections (ELF
- Later:
- concurrency safety beyond A4 (Send/Sync-like rules for shared globals);
- C++ and Objective-C, which pass through to Clang unchanged today;
- reading
lifetimebound,noescapeandlifetime_capture_by.