RFC 0031: The object engine — a sound heap abstraction behind the engine seam
- Status: Superseded
- Authors: WeaveC authors
- Created: 2026-09-28
- Tracking issue: TBD
- Supersedes / superseded by: Superseded by RFC 0036; replaces the engine
of RFC 0030 (
FunctionDataflow, RFC 0030 §15) and the summary format of RFC 0030 §9.1 and §13.1 (summary format 29 becomes 30, unit record format 28 becomes 29). Amends RFC 0030’s assumption A3 (Soundness, below), its §3 certainty rules (restated over objects in §5), its §9.4 boundary facts (§5.6), its §14 seam (§8), and takes over its open gates G10, G14 and G15. Amends RFCs 0002–0017 wherever they describe howFunctionDataflowrepresents a fact; what those RFCs decide about outcomes and diagnostics stays in force as RFC 0030 restated it.
Superseded by RFC 0036, which deletes the advisory analysis this RFC specified. The RFC was Implemented until then (as amended by RFC 0032: unit record format 29 becomes 30, the place class of array-element cells at a boundary (§4.9) is the one RFC 0032’s first amendment states, and the part of gate G6 that runtime extents and liveness close is closed there); it is kept as a record of the design.
This RFC was drafted before implementation. On 2026-09-28 the project owner accepted the recommendation of the milestone analysis (replace the engine behind the seam, together with the engine-independent fixes, and schedule a runtime backstop after it) and authorized drafting this RFC and implementing it end to end in one large change, with breaking changes and deletion of the old engine rather than compatibility layers. Accepted status records that authorization. The status becomes Implemented only when every gate in Acceptance gates passes on the final tree.
Where this RFC departs from the recommendation as it was put to the owner, the text says so with the word Departure and the reason.
Summary
Section titled “Summary”RFC 0030 made WeaveC decide every facet of every memory operation and made
weavec-cc insert checks for what it cannot prove. What it proves is only
as good as its engine, and the engine it kept, FunctionDataflow, stores
facts on access paths (bx.buf, q->buf) and copies them to the aliases
it knows about when the fact is made. An alias it does not know about at that
moment never receives the fact, so the engine proves things that are false:
a heap overflow through a buffer replaced via an alias is proven in
bounds, weavec-cc emits no check, and the program runs past the overflow.
This RFC replaces that engine with the object engine. Facts live on
abstract objects and symbolic values; a pointer is a symbolic value
with a points-to set; memory is a map from object cells to values; copies
share values, so every alias sees every fact by construction. The engine
keeps entry heaps finite by k-limited, materialising entry objects, keeps
loops finite by recency and widening, proves bounds with a zone domain over
symbolic values, and states distinctness of objects explicitly, including a
new owner-forest assumption that makes ownership-based temporal proofs
sound. It publishes through the RFC 0030 seam unchanged in meaning, with a
new, smaller summary format. FunctionDataflow, its 23.7K lines, the
path-based Core trackers and the format-29 summaries are deleted.
The same change fixes the false definite errors and false traps measured on eleven projects WeaveC was never tuned on, and adds those projects to the corpus as a held-out set, so that precision is no longer measured only on code the engine was shaped around.
Motivation
Section titled “Motivation”Measured on 2026-09-28 on v0.11.0 (581670a), Release build, Apple M3.
False proofs. Each was confirmed with AddressSanitizer and with a control
that removes the alias (build/rfc31/probes/, reproduced in
test/cases/soundness/alias-*.c by S0):
struct box { int *buf; };void s3(int k) { struct box bx; struct box *q = &bx; bx.buf = malloc(16 * sizeof(int)); if (!bx.buf) abort(); q->buf = malloc(2 * sizeof(int)); /* replaces bx.buf through an alias */ if (!q->buf) abort(); bx.buf[10] = k; /* spatial: proven; no check; overflow */}struct box *bp = malloc(sizeof *bp); struct n *o = malloc(sizeof *o);bp->a = o; struct box *q = bp; free(o);return q->a->v; /* temporal: proven; use after free */The same programs without the alias are definite errors. The cause is
structural, not a missing case: facts are keyed by PlaceId (an access
path, at most 8 steps), and computeMirrors copies a fact to the aliases
recorded when the fact is made (lib/Analysis/Dataflow.cpp, 161
occurrences of “mirror”). RFC 0030 §5.1 found one instance of this class
(unknown callees) and fixed it with lazy records; the general form remained.
False definite errors on untuned code. Eleven projects built with
CC=weavec-cc (bzip2, hiredis, http-parser, inih, libyaml, lz4, miniz,
mujs, sqlite, tinyexpr, utf8proc): five fail to build as shipped because of
false errors, among them
memcpy(&v[4], v, 4)reported as copying between overlapping ranges (memcpy(v + 4, v, 4)is accepted);fmt(&cmd, …); free(cmd); fmt(&cmd, …); free(cmd);withfmtexternal: definitedouble-freeanduse-after-free, because the write through&cmdby an unknown callee is not modelled;- pointer arithmetic on the stale value after
reallocreported asuse-after-move(libyamlapi.c:81–154); - a back pointer the object’s own release makes unreachable reported as
lifetime-too-shortat link (bzip2bzlib.c:1321);
and three projects trap at run time on correct code, two of them on the
zero-length null arguments of fwrite(NULL, 1, 0, f) and
strncmp(s, NULL, 0), which the library table treats as requiring non-null.
Coverage. Across the corpus (program ledgers), 42% of spatial facets are
unresolved and only about 7% of unproven spatial facets are checked; 49% of
temporal facets are unresolved. The largest reasons are unknown-extent
(5,978), unknown-callee (4,872), dangling-escape (2,570) and
may-alias-released (1,685). The last two come from rules RFC 0030 had to
make coarse because the engine has no object identity: a boundary breaks the
entry assumption for a type (struct cJSON) rather than for the objects
the callee can reach, and a release makes every type-compatible pointer
loaded from a parameter unprovable.
Cost. mujs’s single-file build ran 59 CPU-minutes and 3.7 GB before it was killed (clang: 5.6 s); sqlite3.c did not finish in 15 minutes; Lua’s whole-program analysis takes about 300 s (RFC 0030 gate G15: 214 s). The engine copies its whole state, 43 ordered maps keyed by strings, per block visit and per extra successor, and walks the AST through a place builder that formats declaration names on every field access.
Maintainability. Dataflow.cpp is 11,776 lines; FunctionDataflow has
about 280 methods, 130 members and 35 side tables keyed by AST nodes, and
cites superseded RFCs 633 times. An object representation cannot be added
to it in place: every tracker (Moves, Nullness, Spatial, Resource,
Raw, Borrow) is keyed by path.
The seam RFC 0030 built (§14) exists so that this replacement can happen without touching the ledger, the kinds, the library table, the planner, the emitter or the tests. This RFC uses it.
Soundness
Section titled “Soundness”The guarantee
Section titled “The guarantee”RFC 0030’s guarantee (S), (N), (T), (V) and the blame property are unchanged. Its assumptions A1, A2, A4 and A5 are unchanged. A3 gains one clause:
A3 — other code maintains the heap invariants. Code outside U leaves every pointer U can reach through parameters, results and globals either null or pointing to a live object with at least one element of its type. Pointers in owning slots are unique and acyclic: no object is reachable from itself by following owning slots only (the owner forest). Exact counted-field invariants on header structs that U relies on hold.
An owning slot is RFC 0030’s (§9.4): a field or global some function of
the unit (at link, of the program) releases a value loaded from, or one
declared WEAVEC_OWNED. The new clause is what makes a pointer loaded from
an owning slot of an object distinct from that object and from its owners,
which is what lets a list or tree destructor be proven (§4.5). It is the
invariant Rust’s Box enforces by construction. Code that keeps an owning
cycle (a circular list freed by a counter) breaks it; such code gets
possible findings or unresolved facets where the cycle is visible to U,
and where it is not, the assumption is listed like the rest of A3. The
link step reports what it can verify (§8.3).
What changes about soundness
Section titled “What changes about soundness”- Proofs no longer depend on alias bookkeeping. Every fact a decision reads is looked up through the operand’s value and points-to set at the site (§4.1). A fact made through any alias is on the object, so it is seen through every other alias. The domain’s invariants I1–I6 (§4.7) are what a proof rests on, and each has a unit test that breaks it on purpose.
- Distinctness is explicit. Two pointers are treated as possibly equal unless one of the rules D1–D6 (§4.5) proves them distinct. RFC 0030’s §3.1 list of distinctness rules becomes D1, D2 and D5; D3 (the owner forest), D4 (freshness) and D6 (derivation) are new, and each is as strong as the assumption named with it.
- Boundary facts are per object, not per type. A call breaks the entry assumption only for objects the callee can reach (§5.6). RFC 0030’s type class propagation is kept for what a boundary hands to code the unit cannot see (the other units, at link), where the class is all that is known.
Where this is less sound than v0.11.0
Section titled “Where this is less sound than v0.11.0”Nowhere intentionally. Every decision rule of RFC 0030 §3 keeps its
meaning; where the object engine cannot establish the premise of a proven
outcome, the facet takes the RFC 0030 unresolved reason for the missing
premise. A construct the object engine does not model is unanalysed
(§5.1), never proven.
Two rules become more permissive, each justified by the new A3 clause or by facts the old engine could not see:
- a temporal facet through a pointer loaded from an owning slot of an
object is no longer
may-alias-releasedmerely because some type-compatible object was released (D3, D6); - a call no longer breaks the entry assumption for every object of a type, only for the objects the callee can reach (§5.6).
Where this is more sound
Section titled “Where this is more sound”- The false proofs above, and the whole class they belong to (§4.7, I1).
- Stores and releases through pointers the old engine represented only as “unknown place” (a pointer loaded from an array summary cell, a pointer returned by a callee and stored through twice) now update or taint the objects they may reach, instead of a path nothing else reads.
Bugs caught
Section titled “Bugs caught”Everything RFC 0030 catches (its Bugs caught), with the same severities. Additionally, as definite errors or checks where the values are exact, and as possible findings otherwise:
- accesses through an alias created before the fact (the probes above);
- releases and replacements through pointers stored in heap objects
(
*slot = o; alias = slot; free(o); (*alias)->v); - a use after
decrefof a reference-counted value’s own share, through any copy of the value.
Bugs deliberately not caught
Section titled “Bugs deliberately not caught”As RFC 0030. In addition, where the owner forest is broken by code outside U, a destructor that relies on it may be proven although it frees a node twice. This is A3, not a new blind spot of the engine.
Accepted false positives
Section titled “Accepted false positives”The engine may still give possible findings (warnings) on correct code, in the same classes RFC 0030 lists. It must not give definite findings on correct code in any of the held-out projects (gate G5).
Detailed design
Section titled “Detailed design”1. Scope
Section titled “1. Scope”| Kept (unchanged or edited) | Replaced | Deleted |
|---|---|---|
Ledger, PointerKind, LibrarySpec (+ table), CheckPlan, FnSlots, Diagnostic, SourceLocation, Scc, AnalysisStats, Integer (target integer semantics), Ownership; AttributeReader, KindInference, KindTable, SiteCollector, SlotCollector, BoundaryInvariants, CheckPlanner, LedgerAdapter, Annotations, Concurrency, BypassedDeclarations, UnitPipeline; all of Frontend except the parts named in §8 |
SafetyEngine implementation (DataflowEngine → ObjectEngine); FunctionSummary and SummaryIO (format 29 → 30); SummaryStore; ProgramDatabase import/export of summaries; the unit record’s summary section (format 28 → 29) |
lib/Analysis/Dataflow*.{h,cpp}, PlaceBuilder, FunctionAnalysis, TranslationUnitAnalysis, CallbackSummaries, CallContextSummaries, KindSeeding, FunctionPreparation, AffineSupport, IntegerSupport, Allocators, LibrarySummaries, SummaryDependencies; Core AliasRelation, AnalysisState, Array, Borrow, CallContext, CallTargets, Moves, Nullness, Offset, Place, Raw, Relation, Resource, Scalar, Spatial, Lifetime, Traversal, CheckedInteger, IntegerExpression, SummarySteps, Interface; their unit tests |
SummaryPath (a root and field/deref/index steps) survives: the seam’s
BoundaryFacts, PointerKind’s ExtentTerm and the kinds use it. It moves
to include/weavec/Core/Path.h. What IntegerExpression, Interface or
CallTargets provide that a kept component still needs moves with it; a
kept component may not keep a deleted header alive (hygiene gate H2).
The line budget (H2) is re-derived at the end of S7 from the measured tree and recorded as an amendment, as RFC 0030 did. It is a ratchet: the new engine may not grow past it without an amendment.
2. Architecture
Section titled “2. Architecture” Clang AST + CFG (per function) │ ┌─────────────────▼──────────────────┐ │ lib/Analysis/Engine*.cpp │ transfer: expressions, lvalues, │ (private header Engine.h) │ calls, library rows, summaries, │ │ decisions, witnesses, boundaries └───────┬───────────────────┬─────────┘ │ domain ops │ publishes only through ┌───────▼─────────┐ ┌─────▼─────────┐ │ Core: Heap.h, │ │ LedgerAdapter │ (unchanged seam, RFC 0030 §14) │ Zone.h, Values.h,│ └───────────────┘ │ Persistent.h, │ │ Summary.h (v30) │ no Clang, unit-tested alone └──────────────────┘- Core holds the abstract domain: symbols, values, objects, cells, the
zone domain, join, widening, garbage collection, materialisation,
distinctness, summaries and their text form. It never sees Clang. Objects
refer to program entities through opaque 32-bit handles the Analysis layer
assigns (the
core::SourceLocation::opaquepattern). - Analysis holds the transfer functions over the Clang CFG, the call
dispatcher, the decision rules and the TU driver. It is the only place
that includes
Engine.h. Hygiene gate H2’s include rule moves fromDataflow.htoEngine.h, with the allowed includersObjectEngine.cppandEngine*.cpp. - There is no separate IR. The engine evaluates Clang’s CFG built with
setAllAlwaysAdd(), so every subexpression is an element in evaluation order, and the value of each evaluated expression is kept in a per-block expression environment, as Clang’s own flow-sensitive framework does.?:,&&,||and,then need no special evaluation order. Departure: the recommendation said “a small IR lowered from Clang’s CFG”. Evaluating the always-add CFG gives the same linear order without a second representation to keep in sync with the AST that sites, witnesses and the emitter all name.
3. The engine’s run
Section titled “3. The engine’s run”For each unit:
- Order. Build the call graph over the unit’s definitions (direct
calls, plus indirect calls resolved by the slot solution), take its
strongly connected components in bottom-up order (
core::Scc). - Summaries. Analyse each component bottom up. A non-trivial component iterates its members’ summaries to a fixpoint, with summary widening (§6.4) after 3 rounds and a hard limit of 8 (the limit marks the summaries incomplete, §6.5). These runs publish into a discarding adapter.
- Authoritative pass. Each emitted function (RFC 0030 §2.6) is analysed
once more with the final summaries of everything it calls, publishing
through the unit’s adapter after
beginFunction. A function whose summary run already had the final callee summaries (every function outside a cycle whose callees did not change) reuses that run: its decisions were recorded during the run and are replayed into the authoritative adapter, so a leaf function is analysed once. - Contexts. Alias contexts (§6.6) run after the authoritative pass, publish only diagnostics (RFC 0030 §2.6 Departure), and share the callee’s second budget (RFC 0030 §5.5).
- Exports. The summaries, the kinds and the facts of §7.
weavec --whole-program and the link step run the same algorithm over the
program’s units with the program database (§7), as RFC 0005 and RFC 0030
§13 describe.
4. The abstract domain (Core)
Section titled “4. The abstract domain (Core)”4.1 Symbols and values
Section titled “4.1 Symbols and values”A symbol (core::Sym, 32 bits) names one runtime value that the
function computed or received. Symbols are immutable: a program variable
that is assigned gets a new symbol. Every value the engine handles is one
of:
- an integer symbol, whose numeric facts live in the zone (§4.4);
- a pointer symbol, with attributes (§4.3);
- a function value: a bounded set of function names (at most 32), or unknown;
- unknown of a type: no facts.
Copying a value copies its symbol. That is the whole of the alias mechanism: two variables, cells or expressions that hold the same symbol hold the same runtime value, so a fact attached to the symbol, or to the objects it points to, is seen through all of them.
A value set is a set of at most 4 symbols of one type, for weak cells (§4.2); a loaded value set of more than one symbol becomes one fresh join symbol whose attributes are the join of the members’ and which is recorded as may equal each member (§4.5, D6).
4.2 Objects and cells
Section titled “4.2 Objects and cells”An abstract object (core::ObjectId) stands for one or more runtime
objects. Its origin is one of:
| Origin | Singular | Name | Created |
|---|---|---|---|
local |
yes (per activation) | the declaration’s handle | at function entry for every local whose storage the function uses; its lifetime ends at scope exit (CFG lifetime-end elements) |
global |
yes | the declaration’s handle | on first use |
literal |
yes | the string literal’s handle | on evaluation; read-only |
function |
yes | the function’s handle | on &f |
heap recent |
yes | the allocation site’s handle | by an allocating call (§5.4) |
heap old |
no | the allocation site’s handle | when a heap recent of the same site is allocated again: the old recent object folds into it (recency abstraction) |
entry |
yes | a SummaryPath from a parameter, a global or a callee’s result |
materialised on first load from the entry heap (§4.6) |
entry summary |
no | a SummaryPath prefix and a repeated step |
when an entry path exceeds the k-limit (§4.6) |
unknown |
no | none | the target of a pointer the engine has no facts about (a raw value, an integer conversion) |
A singular object stands for at most one runtime object at a time, so a store through a pointer that must point to it is a strong update and a release of it is a definite release. A non-singular object gets weak updates and may-releases only.
A cell is (object, key). The key is a byte offset within the object,
computed from Clang’s record layout, for fields and constant indices up to
the object’s first 64 elements; a variable index, or a constant one beyond
that, uses the object’s summary cell [*] for its element type (weak).
Amended by §4.9: an affine variable index names a selected cell, and
the summary cell holds only what stores through indices the engine cannot
name wrote.
A union’s members share offsets; a load of a pointer member from a cell
whose last store was of a non-pointer type yields an unknown value with the
raw-cast reason (RFC 0030 §2.3). Bit-fields are integer cells of their
storage unit. A byte-wise write (memset, memcpy, a character-typed
store, a LibrarySpec w argument) over a range of cells replaces them:
by the copied symbols when the copy is a whole-cell copy between objects of
compatible layout, by zero symbols for a zero fill, and otherwise by
unknown values marked raw-cast for pointer cells.
The memory is a persistent map from cells to value sets. A cell that
was never written in this activation holds its entry value: for
local, uninitialised (§5.9); for heap recent, zero or uninitialised by
the allocating row (§5.4); for entry, global and heap old, an entry
symbol materialised on first load (§4.6). Amended (S7): a global with
internal linkage that the unit only reads by value (every reference to it
is a load through subscripts of it and . members, or unevaluated; its
address is never taken or passed, and no store names it) holds its
initializer throughout, so a function-pointer cell of it reads the function
its initializer names there (a dispatch table static void (*t[2])(void *) = {drop, keep} calls keep through t[1]).
4.3 Attributes
Section titled “4.3 Attributes”A pointer symbol carries:
- points-to: a set of at most 8
(object, offset)targets, or⊤(“any object”). The offset is an affine termk·s + cover one integer symbols, or unknown. A points-to set of more than 8 targets becomes⊤; - nullness:
null,nonnullormaybe, with RFC 0030 §3.2’sallocatorSourcebit; - release record: none, or
released {location, family, allPaths, conditional, unknownOrigin, lossy, via}, the RFC 0030 §3.1 record moved from places to values;movedis the same record with reasonmoved; - share count (RFC 0010): the number of references this value holds on a reference-counted object, when it is known;
- raw origin (RFC 0004) with its creation location;
- derivation (§4.5 D6): the symbol and owning slot it was loaded from, when it was loaded from an owning slot;
- names: the C spellings it had where it was created (for messages only, never for identity).
An object carries:
- extent:
bytes = k·s + cwith its class (exact,declared,lower-bound, RFC 0030 §7.1), or unknown; for alocalorglobalof complete type, the constantsizeof; for a VLA, the size symbol; - state:
live,released(with the record of the release that made it so),ended(storage lifetime over),may-released(a weak release or a join ofliveandreleased), orunknown-released(the unknown-callee default, RFC 0030 §5.1, which keeps itsunknownOriginmeaning); - family of its allocation (RFC 0007) and whether this activation owns it (for leaks, §5.8);
- string fact: an affine term bounding the offset of the first NUL, when known (RFC 0012);
- origin facts: read-only (literals), escaped (its address was stored where a callee or another unit can reach it, or passed to one).
4.4 Numbers: the zone domain
Section titled “4.4 Numbers: the zone domain”Integer symbols and the offset and extent terms are related by a zone
(difference-bound constraints x − y ≤ c and bounds x ≤ c, −x ≤ c),
kept closed incrementally (each added constraint tightens the matrix in
O(n²)). Every integer symbol also has an interval in the target type’s
range (RFC 0017 semantics: Integer.h’s modular ranges, kept), and the
zone and the intervals are kept consistent.
- A condition edge (
i < n,p != NULL,x == 3,!flag) adds its constraint to the successor’s state and prunes the edge when the constraint is unsatisfiable. a = b + cwith a constantcrecordsa − b = c; other arithmetic computes intervals only, with wrap-around per RFC 0017.- A comparison of an access
k·i + c₁ + w ≤ k·n + c₂against an extent with the same scale is a zone queryi − n ≤ (c₂ − c₁ − w)/k; different scales fall back to intervals. - The zone is limited to 64 symbols per state. Beyond that the symbols held by the fewest cells lose their relations first (intervals stay), which is sound: it forgets.
4.5 Distinctness
Section titled “4.5 Distinctness”Two pointers may be equal when their points-to sets share a target that
could be the same runtime object. Objects o₁ ≠ o₂ may still be the same
runtime object when both are entry or entry summary objects (or one is
unknown), because the entry heap can alias. They are distinct when one
of these rules applies:
- D1 — types. Their types could not designate the same object under C’s
effective-type rules: neither is a character type and they are not
compatible (all pairs are compatible with
-fno-strict-aliasing). Unchanged from RFC 0030 §3.1. - D2 — owners. Both were loaded from owning places (RFC 0030 §9.4), which A3’s uniqueness keeps distinct. Unchanged.
- D3 — owner forest. One is reached from the other through a path that
contains an owning step:
E(p.next)is distinct fromE(p), and from any prefix ofp, whennextis an owning slot. New, from A3’s acyclicity. - D4 — freshness. An object this activation created (a
local, aliteral, aheap recentorheap oldof a site it executed, a callee’s fresh result) is distinct from everyentryobject, and two fresh objects of different origins are distinct. - D5 — identity. Different singular objects of the same origin kind that the engine created are distinct (two locals, two allocation sites).
- D6 — derivation. A symbol loaded from an owning slot of the object a
symbol
spoints to is distinct froms’s target and from every objectswas itself derived from (transitively, up to depth 8). This is D3 for values that no longer have an entry path, such as the cursor of a loop after a join.
A join symbol (§4.1) may equal each of its members and nothing else they are distinct from.
4.6 Entry objects, k-limiting and materialisation
Section titled “4.6 Entry objects, k-limiting and materialisation”At entry, each pointer parameter pᵢ holds an entry symbol pointing to
E(param i), with the nullness and extent of its kind (RFC 0030 §7.3;
the engine reads EngineInput::kinds, which replaces the old
KindSeeding). A load of a pointer from a cell of an entry object that
this activation has not written materialises E(path.f) (or
E(path[*]) for a summary cell), with the kind of the slot it was loaded
from (RFC 0030 §7.3).
k-limit. An entry path in which the same (record type, field) step
occurs more than twice, or which is longer than 6 steps, is folded into an
entry summary object E(prefix.f+) covering every object reachable from
prefix by one or more f steps. Its cells point back to itself.
Materialisation. When a symbol s whose points-to set contains a
non-singular object Σ is released, or when a pointer is loaded through s
from Σ, the engine focuses: it creates a singular object N with Σ’s
attributes and cell values, retargets s (and every cell and expression
holding s) from Σ to N, and adds N to the points-to set of every other
symbol that points to Σ and is not distinct from s. N’s cells that held
pointers into Σ keep pointing into Σ, which now stands for “the other
objects”. By D3 and D6, a pointer loaded from an owning slot of N is
distinct from N.
Garbage collection. At every join and widening point, objects that are
unreachable from the roots (the function’s locals, parameters’ cells,
globals, the expression environment and the result) are dropped. Dropping a
heap object this activation owns that is neither released nor escaped is
a leak (§5.8). Dropping a released object is how a released list node that
nothing points to any more stops tainting its summary.
Folding. At a loop head’s widening (§4.8), a singular object that was materialised from Σ inside the loop and is still reachable is folded back into Σ (its state and cells join into Σ’s), so the number of objects is bounded by the program’s allocation sites, locals, and the k-limited entry paths.
With these three rules a destructor
void list_free(struct n *p) { while (p) { struct n *next = p->next; free(p); p = next; } }is proven: each iteration materialises the current node, releases it, and
moves to a node D6 keeps distinct; the released node is unreachable at the
loop head and is collected, so no released object is left for p->next to
alias.
4.7 Invariants a proof rests on
Section titled “4.7 Invariants a proof rests on”- I1. Every symbol a cell, variable or expression holds points only to
objects in its points-to set, or into
⊤. A store or release through a pointer updates every object in its set (strong when the set is one singular object and the pointer is not null on the path, weak otherwise). - I2. Two symbols that may denote the same runtime object are equal or not distinct by D1–D6.
- I3. An object’s state is the join of every release that may have reached it on some path, and a symbol’s release record is the join of the releases of that value.
- I4. A cell’s value set contains every value the cell may hold on some path reaching the point; the entry value of a cell never written in this activation is its entry symbol.
- I5. Every zone constraint holds of the runtime values of the symbols on every path reaching the point.
- I6. Garbage collection drops only objects no root can reach, so no later access can name them.
A proven facet (§5) is derived from I1–I6 and the entry assumptions only.
Each invariant has a unit test in unittests/Core/HeapTest.cpp that
constructs the state a violation would produce and checks that the
corresponding decision is not proven.
4.8 Joins, widening and iteration
Section titled “4.8 Joins, widening and iteration”- Join. Objects join by identity (their origin names them
deterministically). A cell present on one side only keeps its value if
the object is absent on the other side (an absent object cannot be
referenced from that side), and joins with the entry value otherwise.
Two different symbols in one variable or cell join into a join symbol
named deterministically by
(block, cell), so iterations reach the same names, with the join of their attributes and the projected zone. - Iteration order. Blocks are visited in a weak topological order (Bourdoncle). Loop heads widen after 2 visits: intervals and zone bounds that grew jump to the type’s limits or to the nearest program constant; points-to sets that grew keep growing up to their bound; materialised objects fold (§4.6). One narrowing pass follows.
- Budget. The block transfers count against
-fweavec-budgetunchanged (RFC 0030 §5.5, default 50,000). A function over budget takes the §2.6 defaults with reasonbudget, and its summary is incomplete. - Persistence. Memory, attributes and the zone are persistent maps
(
core::PMap, sorted vectors shared by reference count and copied on write), so a state is copied per edge in O(1) and a join costs the size of the difference, not of the state.
4.9 Elements and ranges (Amendment (arrays))
Section titled “4.9 Elements and ranges (Amendment (arrays))”Added during S2. One weak summary cell per element position made every
read of a[i] the same symbol, so free(a[i]); *a[j] = 1; was a definite
use after free inside zap (evaluation rfc0016-array-good), and it could
not express RFC 0015’s release history: every cleanup loop
for (i = 0; i < n; i++) free(a[i]); warned “may be freed twice”, and no
summary could say that a helper released elements [0, n). The array
domain is therefore RFC 0015 §1–§5 over objects, as a segmentation of each
element position:
- Selected cells. A load or store at byte offset
k·s + cfor an integer symbolsuses the cell(o, k·s + c). Symbols are immutable, so two accesses with the same key are the same runtime cell, and a saved index (old = i; i = 7; a[old]) still names the element it named. Two keys whose offsets the zone makes equal are one cell; offsets it proves different, or with different residues modulo the element size (other fields), are different cells; otherwise they may be the same cell. At most 32 selected cells per object. - Summary cell. Holds only the values of stores through positions the
engine cannot name (an unknown offset, a callee’s
[*]store, an evicted cell or range). A missing summary cell is “nothing written that way”, so a join keeps a summary cell one side has. - Segments. An object keeps, per element position (offset within the
element, element size), a list of ranges
[from, to) ↦ v, newest first (at most 4 per position).fromandtoare element-index terms over symbols;vdescribes each element of the range: a read copiesv’s attributes into a fresh symbol (the elements are different runtime values), and a definite release record onvsays every element in the range was released.vjoins two such values keeping a record definite only when it is definite on both. - Reads. An element’s value is its own cell; else a cell the zone makes
equal; else a copy of the newest range that must contain it; else the
unwritten value (§4.6) joined with the summary cell. Cells and ranges that
may be the element contribute their values as possible ones: a release
record keeps its evidence as a possible release (RFC 0015 Accepted false
positives: an index that may select a released cell is reported as a
warning), while the summary cell’s contribution is
aliasOnly(no diagnostic, never proven). A read of “some element” (a callee’s[*]path) joins everything at the positionaliasOnly. The objects that elements of an entry array point to,E(p[*]), are not singular. - Writes. A store to an element cell is strong for that cell when the object is singular and single; cells and ranges that may be it take the value weakly; a range that must contain it is shadowed by the cell. A store through a summary key reaches every cell and range at its position weakly.
- Eviction. A selected cell whose index symbol a join cannot keep, or a range that matches nothing on the other side of a join, is removed as a weak store of its value to the elements it described (the summary cell and the ranges that may hold them). Nothing an element holds is dropped without such a store, so the segments and cells only refine the summary cell and the unwritten values, and the decisions of §5.2 rest on I1–I6 as before.
- Joins. At a loop head, the element cells the iteration changed (on
the back edge, a new cell, another value, or the same value with other
facts such as a release) fold into ranges: a cell at the index a range
ends at extends it (
[lo, i) + a[i] → [lo, i + 1)), otherwise it becomes a one-element range; the head’s own cell for that element is evicted. At every join, an element cell one side has only is matched with an element cell of the other side whose index the join pairs (the first iteration’sa[0]with a later iteration’sa[i]: one cella[i']of the join); otherwise it is materialised on the other side, or evicted when that side’s ranges cannot tell it apart. Ranges are aligned per position from their oldest ends; a bound of the joined range is a result symbol of a pair the join makes anyway (the variables’ values, with a constant offset), or the pair of the two bounds’ own symbols. A range only one side has is kept when the pairing makes it empty on the other side (the loop’s first entry:[0, i)withi = 0), else it is evicted. Every choice is sound; they differ in precision only. - Entry objects across a join. An entry object (or its dead copy) that
one side released and the other never materialised is
may-releasedin the join: the other path left it live. (Before this amendment the join keptreleased, and a loop’s first iteration maderelease *a alwayspart of the summary of a cleanup that may run zero times.)
Copies (RFC 0015 §4). memcpy/memmove of elements copy element-wise
by the element type the arguments point to (pointer and integer leaves of a
record), reading every source cell before writing any (so overlapping moves
are simultaneous). A symbolic length copies exactly the elements its lower
bound covers and writes the rest weakly. Amended (S7): when the source is
an entry object no store has reached in the activation (not havocked, not
the destination), the rest is instead a copied range of the elements the
length counts (n for n * sizeof *s without wrap, else a count symbol no
smaller than the lower bound’s): each element holds the entry value of its
own source element, which is the symbol a load of that element reads (or
read, found by the cell it was materialised for), so free(s[0]) after the
copy is seen through d[0]. An element the range may hold gets that value
as a possible one. Any later change to the range (a weak store, a fold, a
join) makes it the plain range its value describes (some element of the
source), so the refinement never outlives what it rests on. realloc’s new block starts with
the old block’s cells, selected cells and ranges below its size; pointers
into the old block do not follow it.
Summaries (§6.1). An entry object’s ranges and selected cells at an
exit become release <p>[*]* elements [<from>, <to>) <family> when their
value is the elements’ own entry values released definitely, and `store
[*] elements [ Known limits. A permutation of an array’s own elements through
variable indices is not exported. The zone cannot keep An rvalue evaluates to a value (§4.1); an lvalue evaluates to an address,
a points-to set with an offset. Loads and stores go through the address: A construct the engine does not evaluate (atomic builtins beyond load and
store, vector types, The engine decides each facet of each site Temporal (Deref, Index, Call arguments, Release, Raw): Null (RFC 0030 §3.2): Spatial (RFC 0030 §3.3, §7.4): for each target A target set with several objects is checked only when every target’s
check is the same C expression (same extent term); otherwise it is
Unsafe regions, A checked facet needs a witness (RFC 0030 §14, In verify mode the engine publishes witnesses for proven spatial and null
facets too (RFC 0030 G6). A call’s callee is resolved in RFC 0030’s order (§5.1–§5.3, §9.3): Indirect calls resolve through the flow-sensitive function value, then the
slot solution (RFC 0030 §9.3, unchanged). Several targets join their
summaries (a consume is definite only if every target consumes). Library rows. A row’s Zero-length arguments. A buffer argument whose required length is a
term that may be zero is Unknown callees (RFC 0030 §5.1, unchanged in meaning, now per object):
for each pointer argument without an ownership contract, the call marks
every object reachable from the argument’s targets through non- Callee summaries are applied by instantiation (§6.3). Callbacks, threads and signals keep RFC 0030 §5.3: a Non-returning calls end the path; the call’s exit boundary (§5.6) is
checked first. A release of value Shared ownership (RFC 0010): a At every boundary RFC 0030 §9.4 names, the engine publishes Exits: at an exit that returns to the caller, only storage whose lifetime
ended is reported (RFC 0030 §9.4 amendment 1); the result counts (amendment
2). A leak (RFC 0007, never an error, RFC 0030 §3.4) is reported when garbage
collection (§4.6) drops an owned A Scalar Every diagnostic id, message template and note of RFC 0030 is kept word for
word; the lit tests that pin them stay. Names in messages come from the
symbol’s A summary describes a function’s effect on its entry heap (parameters and
globals, as The fields format 29 carried for the old engine alone (heap descriptions,
value snapshots, array copy/fill/release forms, numeric output expressions,
memory and callback context requests, object views, sized-field facts,
count lists) are not carried. What each fed is either derived at the call
from the fields above (array forms from At every exit of the authoritative or summary run, the engine reads the
state against the entry heap: an entry object that is At a call, the callee’s parameter paths are matched against the caller’s
state: Amendment (heap outputs). Added during S6 for RFC 0013’s heap outputs,
which the first format-30 summaries lost: A component’s summaries are iterated from “no effect”; after 3 rounds, a
summary that still grows widens (paths that keep lengthening fold at the
k-limit; integer terms lose their bounds). After 8 rounds the remaining
summaries are marked incomplete. RFC 0030 §5.5: a caller applies an incomplete summary’s known effects plus
the unknown-callee default on every pointer argument. A summary is derived assuming distinct parameter objects except where D1–D6
cannot separate them; where they cannot, the entry objects are joined
already and the summary is sound for aliased calls. Where a caller passes
arguments that must be the same object to parameters the callee treated
as possibly distinct, and the callee Amendment (numeric contexts). A summary cannot say what a callee does
with values it does not know: The link step already checks owner uniqueness at boundaries it can see
( The rows of §5.4 get the zero-length form. Each changed row gets a unit
test in RFC 0030 G14 measured that Lua’s interpreter loses its dispatch
replication when checks supply the edges into a block ending in
RFC 0030 §3.4 is kept: a definite violation lowered with
Listed in §1. The Core and Analysis unit tests of deleted components go
with them; their scenarios that describe behaviour (not representation)
are converted to Every case in No flag changes except: Per function, cost is the number of block transfers times the size of what
changes. The expected costs, which gate G13 bounds: None. None added or removed. Every id keeps its severity rules (RFC 0030 §3) and
message templates. The unresolved and trust reasons are RFC 0030’s closed
lists; none is added (§8 Departure). The stages land on branch Fallback. If S4 cannot reach G1–G4 with the object engine, the change
does not ship with the old engine deleted: the branch is re-planned with the
owner, and the engine-independent fixes (§9) and the new tests (S0) are
offered as a separate change. There is no configuration in which both
engines ship. Binaries are the Soundness Parity and recall Precision Codegen and cost Hygiene The stages recorded the decisions below as they were implemented. Each
amends the section it names; where the text above and an amendment
disagree, the amendment holds.i ≤ n across a
loop whose signed counter starts above a possibly negative n, so such a
range is exported without bounds. n * sizeof(T) of an unbounded n may
wrap, so it gives no count of copied elements.5. Transfer and decisions (Analysis)
Section titled “5. Transfer and decisions (Analysis)”5.1 Expressions and lvalues
Section titled “5.1 Expressions and lvalues”
x (a local, parameter or global): the cell (object(x), 0);*e, e->f, e[i], e.f: the pointer value of e plus the field’s
byte offset or the index times the element size;&lv: the address as a pointer value (a fresh symbol pointing to it);p + i, p - i, &p[i], ++p: a fresh symbol with the same targets
and the offset term moved; p - q over one object is the difference of
the offsets;unknown; a pointer-to-integer conversion keeps the
pointer symbol behind the integer, so the round trip is recognised;local objects of their scope; string literals are
literal objects; sizeof of a VLA is the VLA’s size symbol.__builtin_* without a table row, statement
expressions containing jumps) yields unknown values and, for every site it
contains, the unresolved reason unanalysed with the construct named in
the detail.5.2 Decisions at sites
Section titled “5.2 Decisions at sites”SiteCollector enumerated
(RFC 0030 §2.1) from the operand’s value at the site. The tables of RFC
0030 §3 apply unchanged; their premises are read from the object domain:
Facts about the operand’s value
v at the siteOutcome
Diagnostic
v has a definite release record (allPaths ∧ ¬conditional ∧ ¬unknownOrigin)violation
use-after-free / use-after-move / double-free, error
v’s record is not definite and not unknownOriginunresolved(may-released) / (may-moved)same id, warning
every target of
v is released and singular, with definite recordsviolation
as above
some target is
released/may-released and v has no record of its ownunresolved(may-alias-released)none
some target is
unknown-releasedunresolved(unknown-callee) or (callback)none
some target is
endedviolation if every target is ended and singular; else
unresolved(may-dangle)lifetime-too-short as RFC 0030 §3.4
otherwise
proven
none
null without allocatorSource → violation
(null-dereference); nonnull → proven; anything else → checked (with
allocation-failure when allocatorSource). Refinement after a
dereference follows §3.2: the symbol becomes nonnull downstream only when
the facet was proven or checked.(o, off), the access
[off, off + width) is compared with o’s extent in the zone:
Result over all targets
Outcome
in bounds for every target
proven
out of bounds for every value on every target, extent exact
violation (
out-of-bounds)
undecided, and one target whose extent is exact or declared and expressible (§5.3)
checked
extent unknown or lower-bound only
unresolved(unknown-extent)
offset unknown against a known extent
unresolved(unknown-index)
⊤ points-tounresolved(unknown-extent)unresolved(unknown-index). RFC 0030 §7.4’s object rules (flexible
trailing arrays, complete-object extents for interior pointers, byte
arithmetic without wrap) are how the extent and offset terms are formed.
The overlap rule of disjoint requirements compares the two argument
ranges in the zone: &v[4] and v with length 4 are disjoint, which fixes
the lz4 false error.setjmp, concurrency, assumptions and require levels
(RFC 0030 §5.3, §5.4, §6) are applied exactly as there.5.3 Witnesses and C names
Section titled “5.3 Witnesses and C names”CheckWitness) whose terms
name C entities at the site. The engine keeps, per state, a reverse map from
integer and pointer symbols to the C places that currently hold them: a
local, a parameter, or a field path below one reached only through
non-escaping locals. A term over symbol s is expressible at the site when
some C place holds s there; the witness names that place. Because symbols
are immutable, a C place that holds s at the site holds the value the
extent was derived from, which is RFC 0030 §10.3 rule 4 without a separate
“unmodified” analysis. When no C place holds s, the witness carries the
symbol’s defining expression if it is side-effect free and its operands are
themselves nameable, and is otherwise inexpressible.5.4 Calls
Section titled “5.4 Calls”
--whole-program);LibrarySpec row;trusted(system-api);alloc creates a heap recent object of the
row’s family with the extent the row’s size expression gives, zeroed when
the row or zero-initialisation says so; its result symbol is
maybe-null with allocatorSource unless the row says nonnull. A
release releases the argument’s value and its targets (§5.5). Argument
requirements (r:bytes(n), nonnull, string, disjoint) become
requirement records on the LibCall or Release site with the decision rules
of §5.2. realloc-like rows keep RFC 0030 §8.2’s outcome classes: the
argument is released on the nonnull class and on the null class when
the size is zero.null-if-zero: its null requirement applies only
when the length is non-zero, and its check is the zero-length nonnull
form. The table’s rows for fwrite, fread, memcpy, memmove,
memcmp, memset, strncmp, strncpy, strncat, strnlen, memchr,
snprintf, vsnprintf, write and read are corrected accordingly
(§9.1). This removes the miniz and mujs false traps. Departure: C
leaves memcpy(NULL, p, 0) undefined; RFC 0030 §8.3 already chose the
zero-length form for memcpy’s fold case, and this RFC extends it to every
row whose length can be zero, because the trap protects no memory.const
pointees, every escaped object and every object reachable from a global
external code can reach as unknown-released and forgets their cells
(they read as fresh entry symbols afterwards); the argument’s own value
keeps its nullness and extent. A pointer argument that is the address of a
cell (&cmd) makes that cell written by the callee: after the call it
holds a fresh symbol of unknown nullness and no release record, which is
what the hiredis fmt(&cmd, …) pattern needs. The result is a fresh
maybe symbol pointing to unknown with the Single default (A3).sync callback
applies its targets’ summaries as may-effects; entry targets define the
set G of shared globals and objects whose facets get the concurrency rules.5.5 Releases
Section titled “5.5 Releases”v (a LibrarySpec release row, a summary’s release
effect, WEAVEC_RELEASES):
v or its targets already released → double-free
per §5.2’s table; an unknown-released target → unresolved(unknown- callee), and the record is replaced (RFC 0030 §3.1, a known release
after an unknown one).invalid-release). A target that is not heap,
entry or unknown (a local, global or literal), or whose offset is
provably non-zero, is a violation when it holds for every target, and
unresolved(may-invalid-release) with a warning otherwise. The messages
are RFC 0030’s ('<p>' is released but points to field '<f>' of its allocation, …).mismatched-release). Compared as RFC 0030 §3.4.v gets the release record (allPaths = true on this path).
Each target becomes released when v must point to that one singular
target, and may-released otherwise. Every other live symbol whose
targets may equal a released target (§4.5) is left as it is: the object’s
state is what makes its later uses may-alias-released, and the symbol’s
own record stays empty, so no diagnostic is made from aliasing the engine
cannot confirm. RFC 0030 §9.4 item 4 (interior aliases are not owners) is
kept: releasing an object through an interior pointer does not release the
enclosing object’s other names.conflicting-borrow (RFC 0002):
an error when that cell holds the pointer on every path and the
release is unconditional, a warning (unresolved(may-conflict))
otherwise.decref row or summary effect decrements the
value’s share count and releases the share: the value gets a release
record with the message wording after its reference was released; the
object becomes may-released unless the engine knows the count reached
zero. An incref increments it. The count-field keys of RFC 0010 are
exported as before.5.6 Boundaries
Section titled “5.6 Boundaries”BoundaryFacts
over the objects the callee can reach: the objects reachable from the
arguments’ targets and from every global external code can reach, through
the memory. A reachable cell that may hold a released or ended pointer and
is not overwritten before the boundary is a Dangling fact; two reachable
owning cells that may hold the same object are SharedOwners. Places are
spelled as SummaryPaths from the boundary’s arguments and globals, and
each fact carries the RFC 0030 place class of the cell.BoundaryInvariants is unchanged. What changes is what reaches it:
strm.state->strm back pointer, freed together with state,
is not reachable by the next call’s arguments);entry object not distinct from it). The
type-class-only rule remains for rows that come from other units at link,
where objects are not shared.5.7 Lifetimes
Section titled “5.7 Lifetimes”local objects end at their scope’s CFG lifetime-end element and at
function exit. A pointer to an ended object:
lifetime-too-short (definite when every target is ended and
singular and the cell must hold it), else unresolved(may-dangle);test.c:2687 false error);5.8 Leaks
Section titled “5.8 Leaks”heap object that is neither released nor
escaped, at the statement that made it unreachable: an overwrite of the last
cell holding it ('<p>' is leaked: it is overwritten without being released), a scope exit, or a return ('<p>' is leaked). Not at a return
from main or after an exits row (RFC 0030 §3.4). An object passed to a
callee that may retain it, or stored in a cell another unit can reach, is
escaped.5.9 Initialisation
Section titled “5.9 Initialisation”local pointer cell that was never written holds uninit. A use where
every value is uninit:
use-of-uninitialized when every path is
uninitialised, checked otherwise (RFC 0030 §3.1 table);-fno-weavec-zero-init, unresolved(no-zero-init).uninit uses keep RFC 0030 §3.4’s rule (error when definite, no
facet).5.10 Integer operations
Section titled “5.10 Integer operations”invalid-integer-operation (RFC 0017) is reported when the operands’
intervals make the operation invalid for every value (division by a zero
interval, a shift count interval outside the width, a signed overflow for
every value). Possible invalid operations are not reported (unchanged).5.11 Diagnostics and messages
Section titled “5.11 Diagnostics and messages”names (§4.3), preferring the operand as written at the site
('q->a'), then the name the value was created under. The notes (freed here, freed here (through '<p>'), allocated here, …) come from the
records.6. Summaries (format 30)
Section titled “6. Summaries (format 30)”6.1 Content
Section titled “6.1 Content”SummaryPaths) and its result, per outcome case:<case> is RFC 0030 §9.1’s grammar (always, result <classes>,
result <classes> and param <i> =0|!=0), extended by the pointer
comparison and entry test amendments below; the at-most-two-cases rule, the
lossy bit and the pruning amendment of RFC 0030 §9.1 are kept. Paths use
the SummaryPath spelling of format 29. Summary format 30 is not readable
as 29 or the reverse; a record of another version is stale (RFC 0030 §13.1).store/release over [*] steps;
sized fields from result fresh … extent), or dropped with its cases
listed in test/cases/KNOWN-DIFFERENCES.md.6.2 Derivation
Section titled “6.2 Derivation”released, moved
or unknown-released gives the corresponding effect for its path; a cell of
an entry object or global that was written gives a store; a result
pointing to a fresh object gives result fresh, with the extent term
re-expressed over parameters when the zone relates it to one. The cases
come from the result’s value on each exit path (RFC 0030 §9.1 derivation,
with the guard over symbols instead of places). Exits join into at most two
cases per effect.6.3 Instantiation
Section titled “6.3 Instantiation”param i is the argument’s value, each step loads through the
caller’s memory (materialising entry objects as a load would), and each
effect is applied to the objects the path reaches in the caller, weakly
when the path reaches several. A result fresh becomes a heap recent
object named by the call site. A case keyed on the result becomes a pending
case on the result symbol, resolved by a later test of it (RFC 0030 §9.1’s
PendingOutcome behaviour, now on the symbol).
store of a new object into an entry
cell is followed by the object’s contents as store … contents=<n>
(spelled (new) in dumps): the path lies below the new object that the
store to its first n steps puts there. At a call, the dereferences of a
contents path from step n on read the values the summary stores (the
new objects, created before any store is resolved), never the entry
heap’s; one with no such value is not applied. Paths of stores into entry
objects keep reading the caller’s state before the call, so the two never
name the same cell by accident. A cell that holds its entry value on some
paths and a new object on others is that object, stored may; a cell null
on some exits and a new object on the others holds the object
maybe-null, and a null test of it in the caller tells the failure, which
made no object.store result.<f> … (fields without a dereference), when every exit
returns storage of the callee’s frame whose cells it knows; the caller’s
temporary for the call then holds exactly those cells (an unnamed field
was never written). Otherwise the temporary holds unknown values.fresh … offset <c> is a pointer c bytes into the new
object (a cursor stored beside its base); unknown raw is a raw pointer
(RFC 0004), which stays raw in the caller. An unknown value in a new
object’s cell is stored, never omitted (the caller would read the object’s
unwritten, zero value). Member paths spell nested records
(p->box.data), so a caller resolves them.string <path> nul-within=<t> [nul-from=<t>] [contents=<n>] gives an object’s RFC 0012 string fact relative to where
its pointer points, on every exit where the object exists.Stored pending case). The join
stays when the call may release the old or the new value under a
condition no case names: that release is then an alias’s (§5.5), not a
possible finding on the path where the store did not happen.6.4 Recursion and widening
Section titled “6.4 Recursion and widening”6.5 Incomplete summaries
Section titled “6.5 Incomplete summaries”6.6 Alias contexts
Section titled “6.6 Alias contexts”reads one after releasing the other,
the caller requests an alias context: the callee is re-analysed with those
entry objects unified, its diagnostics are reported at the use in the
callee with a note naming the call (RFC 0030 §2.6 Departure, probe
two(p, p)), and the call’s temporal facet takes the result. At most 16
contexts per callee, sharing the callee’s second budget.unsigned char narrow(unsigned n) { return n; } returns int [0, 255], and make(&n) allocates *n * 2 bytes of an
*n the summary cannot name. RFC 0017 §5 requires a narrowed result, a
checked size through an out-parameter, and a size computed from an input to
be preserved in the caller; format 30 carries no numeric expressions
(§6.1). So at a direct call to a function of the unit outside the caller’s
component, whose general summary returns or stores an integer that is not
one constant, allocates a size it does not know, or has a may, lossy or
parameter-keyed effect, and whose call knows integers the callee reads (an
integer argument that is a constant, or an integer cell of the single object
a pointer argument, or a pointer global the callee names, points to), the
engine analyses the callee again in that numeric context: its alias
context (above) with those parameters bound to their constants and those
entry cells to theirs. A call whose alias context
is not trivial is analysed in it whatever its summary says. The run is a
summary run (it reports nothing) and the call instantiates the summary it
derives instead of the general one; the summary is sound for the call
because its entry state is the call’s. Contexts are cached per callee (at
most 8), nest at most two deep, and are run only for callees whose own run
took at most 64 block transfers; a context run that is over budget or
incomplete leaves the general summary. Two pointer arguments are the same
object only when they point to one singular object.7. Records, the program database and the link step
Section titled “7. Records, the program database and the link step”
UnitExports loses memoryRequests, callbackRequests, sizedFields,
sizedFieldLoads and unknownIndirectTypes; it keeps functions
(with format-30 summaries), globals, imports, indirectTypes,
unknownCallees, countFields and boundaries, and gains
contextRequests (§6.6).ProgramDatabase imports format-30 summaries by linkage name and type key
as before..#2) instead of an empty name, so a
function that reads one produces a record that decodes (the tinyexpr
te_eval stale-record bug).ObjectEngine runs.7.1 The seam
Section titled “7.1 The seam”EngineOptions::analysis (AnalysisOptions, FunctionDataflow’s own
tunables) is removed; the options it held that are not engine-private
(dumpStream, stats) become fields of EngineOptions. SafetyEngine.h
no longer includes FunctionAnalysis.h or Summaries.h. facetOfDiagnostic
moves into LedgerAdapter as a table keyed by the diag:: constants.
storeVerdict and EngineInput::fieldAssumptions stay (RFC 0030 §7.6 may
return), unused.8. The owner forest at link
Section titled “8. The owner forest at link”second-owner, RFC 0030 §9.4). The engine adds to each boundary’s facts
an OwningCycle fact when a reachable owning cell may hold a pointer to an
object from which the cell’s own object is reachable through owning cells
(a store n->next = n, or a->next = b; b->next = a visible in one
function). BoundaryInvariants treats it like SharedOwners: the boundary
is unresolved(second-owner), with detail “owning cycle”, and its class
propagates. Departure: a new unresolved reason owning-cycle would be
clearer, but the closed list is RFC 0030’s and second-owner already means
“the ownership invariant of A3 is broken here”.9. Engine-independent fixes
Section titled “9. Engine-independent fixes”9.1 The library table
Section titled “9.1 The library table”unittests/Core/LibrarySpecTest.cpp (one per row, as RFC 0030 §8
requires).9.2 The computed-goto layout cliff (RFC 0030 G14)
Section titled “9.2 The computed-goto layout cliff (RFC 0030 G14)”indirectbr, and that splitting those critical edges restores it
(1.4783 → 1.10). weavec-cc runs Clang in process, so it registers the
split through CodeGenOptions::PassBuilderCallbacks (no pass plugin, no
PassPlugin.h): at OptimizerLastEP, a function pass splits every critical
edge into a block that ends in indirectbr and has more than 8
predecessors. The callback is registered only when checks are emitted, so
-fweavec-checks=none objects stay identical to Clang’s (G7).9.3 Lowered violations
Section titled “9.3 Lowered violations”-Wno-error=weavec-<id> still traps at the site. Departure: the
recommendation put to the owner listed “no trap left behind a lowered
error”. That would break guarantee (V), which every enforcing build states.
The cause of the libyaml and lz4 traps was the false errors themselves,
which this RFC removes (§5.2, §5.4, §5.7); a baseline and waiver mechanism
for errors a user decides to accept belongs with RFC 0033’s baselines
(Future work), where a waived site can be recorded as trusted with a
reason rather than silently unguarded.10. Deletions
Section titled “10. Deletions”test/cases/semantics/ or to unittests/Analysis/ ObjectEngineTest.cpp first. The superseded RFCs keep their text.
docs/architecture.md, docs/development.md, the roadmap and the
docs-site pages that describe the engine are rewritten.11. Tests
Section titled “11. Tests”11.1 New cases
Section titled “11.1 New cases”
test/cases/soundness/alias-*.c: the probes of Motivation (p3–p7 of
the analysis and their controls), each with the ASan-reported line
marked, and correct twins.test/cases/repros/ooc-*.c: one reduced case per false error and false
trap of the held-out projects (overlap by &v[k], unknown callee writing
through &cmd, realloc rebasing, back pointer freed with its owner, global
dangling then overwritten, fwrite(NULL, 1, 0, f), strncmp(s, NULL, 0), anonymous-member record round trip).test/cases/semantics/objects/: the domain’s cases: strong and weak
updates, recency in loops, list and tree destructors (proven),
materialisation, owner-forest cycles (not proven), array summary cells,
unions, byte-wise copies of pointer structs.11.2 The held-out corpus
Section titled “11.2 The held-out corpus”test/corpus/manifest.json gains the eleven projects of Motivation as
configs marked "heldOut": true, pinned by SHA, with their builds and test
suites. Held-out configs are measured and gated (G5, G12, G13) but their
triage entries may only record verdicts, never motivate an engine rule that
names them (H2 already forbids naming a corpus project in lib/). sqlite
and mujs are compile-and-time configs (G13); their test suites are not run
by the gate.11.3 Existing tests
Section titled “11.3 Existing tests”test/cases keeps its markers, except where the object
engine’s result is at least as strong and the marker is updated with the
reason in the commit (a possible that became definite on an exact
alias, an UNRESOLVED that became proven with a proof the case’s comment
supports). A marker the object engine no longer meets is listed in
test/cases/KNOWN-DIFFERENCES.md with its reason; the list is bounded by
gate G2. The lit tests that pin messages stay; those that pin the old
engine’s dump format or summary text are rewritten for format 30 and the new
dump.12. Command line
Section titled “12. Command line”--dump-analysis prints the object engine’s states
(objects, cells, symbols and zone at each block exit and site; unstable
format), and --analysis-stats reports block transfers, joins,
materialisations, objects and zone sizes.13. Performance
Section titled “13. Performance”
Annotation surface
Section titled “Annotation surface”resources/include/weavec.h is untouched.Diagnostics
Section titled “Diagnostics”Implementation plan
Section titled “Implementation plan”rfc0031-object-engine as checkpoint commits and
ship as one change. The old engine stays buildable beside the new one until
S6, selected by an internal environment variable only the test scripts set,
so every stage can compare both on the same inputs; S6 deletes it.
Stage
Work
Gate to leave the stage
S0 Tests first
The alias probes, the held-out repros and
semantics/objects/ cases (§11.1), expected to fail on the old engine where it is wrong; the held-out corpus configs (§11.2); a baseline run of every gate on v0.11.0 recorded in the PR.The new cases fail on the old engine exactly where the analysis says; the held-out configs build with the reference compiler.
S1 Domain
Core:
PMap, symbols and values, objects and cells, attributes, the zone, distinctness, join, widening, GC, materialisation, folding (§4); unit tests for each, including I1–I6.WeaveCCoreTests pass under ASan; the domain has no Clang include.
S2 Intraprocedural engine
ObjectEngine over the always-add CFG: expressions, lvalues, loads, stores, casts, locals, globals, literals, conditions, loops, scopes; decisions for Deref, Index, PtrArith, Cast, IntToPtr, Raw and Assume sites with witnesses (§5.1–§5.3); budgets. Library calls only for allocation and release.The alias probes are caught;
run-cases.py --filter 'pairs/**' --filter 'proofs/**' passes under the new engine; no proven facet at an ASan-reported line in any case.
S3 Calls
The full call dispatcher (§5.4): library rows, unknown callees, system APIs, slots, callbacks; format-30 summaries: derivation, instantiation, cases, recursion, incompleteness (§6.1–§6.5).
evaluation/** and recall/** pass under the new engine.
S4 Temporal completeness
Releases, families, invalid releases, conflicting borrows, shares (§5.5), boundaries and the owner forest (§5.6, §8), lifetimes (§5.7), leaks (§5.8), initialisation (§5.9), integer operations (§5.10), alias contexts (§6.6), concurrency and
setjmp.Every
test/cases suite passes under the new engine, with the marker changes of §11.3 and within G2.
S5 Records and link
UnitExports, format-29 records, ProgramDatabase, the link step and --whole-program over the new engine (§7).test/WholeProgram and the multi-unit cases pass; G11 measured.
S6 Delete
Remove the old engine and everything §1 lists; the hygiene gate’s include rule and line budgets; lit and unit test migration (§10, §11.3).
Build with warnings as errors; every unit, lit and case suite passes; H1, H2.
S7 Fixes and cost
§9.1, §9.2; profiling against G13; docs.
G1–G13 and H1–H3 on the final tree.
Acceptance gates
Section titled “Acceptance gates”release preset’s weavec and weavec-cc on the final
tree; the reference compiler is $WEAVEC_LLVM_PREFIX/bin/clang (LLVM 23).
Timing gates are measured on an idle machine (load average below 2 at the
start of each run), and the figures in the PR say which machine.
scripts/run-cases.py --asan over every suite: 0 cases in which
an ASan-reported bug line has its matching facet proven; the alias
probes (§11.1) are each reported (error, warning, trap or non-proven
row); --checks verify over every executable case gives 0
weavec.proven traps. The same --checks verify holds for the test
suites of the eleven original corpus configs and the held-out configs
that have test suites (corpus-gate.py --full --checks verify).
test/cases suite passes under run-cases.py (trap mode,
--asan). Markers changed per §11.3; at most 20 entries in
KNOWN-DIFFERENCES.md beyond the ones it lists today, none of them a
BUG marker in evaluation/, pairs/ or soundness/.
fclose(stdout));
possible temporal warnings at most 60 in total (RFC 0030 G10, which is
failing today with 652); 0 traps in the project test suites (RFC 0030
G11).CC=weavec-cc
and runs its test suite with 0 traps, except where a definite error is
triaged true with source evidence; 0 definite errors triaged false.expected.json as a
ratchet. RFC 0030 G13’s per-file limits (linenoise 0.25, cJSON 0.25, sds
0.60) hold.
-fweavec-checks=none,
at least 100 TUs × 3 configurations).make -j8 with CC=weavec-cc at most 5.4 s wall on the
reference machine (RFC 0030 G15).weavec-cc build takes at most 8× the
reference compiler’s user CPU; mujs’s single-file one.c and sqlite’s
sqlite3.c each compile within 15 CPU-minutes and 4 GB.expected.json (the ratchet’s cpuSeconds) and is not above v0.11.0’s
by more than 10% for any config.
scripts/check-hygiene.py: no includer of Engine.h outside
ObjectEngine.cpp and Engine*.cpp; no deleted header included; no
FunctionDataflow, PlaceBuilder, AnalysisState, MoveTracker or
computeMirrors in lib/, include/, tools/, unittests/ or
docs/ outside docs/rfcs/; the corpus-name and LibrarySpec-name rules
of RFC 0030 H2; Engine*.{h,cpp} and the Core domain headers at most the
budget §1 records; the code under lib/, include/ and tools/ at most
80,000 lines (LibrarySpec.txt excluded; 86,774 today).npm test && npm run build in docs/ pass; the architecture,
development and roadmap pages describe the object engine; RFC 0030 is
marked Implemented as amended by this RFC once G1–G13 pass, and this RFC
is marked Implemented.Implementation amendments
Section titled “Implementation amendments”
dead bit), so the released object stops overlapping the
live one of the next iteration while the summary still sees the release
(the dead copy keeps the path). Loops that walk owning links (the list
and tree destructors) stay unresolved where a focus object must stand
for a node its candidates already released: see Unresolved questions.int
against an unknown unsigned), it proves nothing, and keeping it related
every symbol to every other (a third of Lua’s whole-program time).
Demotion is one pass over the rows, and a join pairs only the result
symbols some side’s stored bounds relate (or that share a symbol on a
side), not every pair of result symbols. The
values a block read from earlier blocks
(§2’s cross-block expressions) leave the state on its out-edges, except
the operands of a conditional or logical operator, whose untaken side’s
last value the operator’s join still reads.x - y <= upper(x) - lower(y), below
2^31), and a query adds that back. Closing over zero had related every
bounded symbol to every other: each new constant cost a pass over every
pair of symbols (an unrolled checksum loop took 3 s for 229 block
transfers). Adding x - y <= c now visits only the symbols with a stored
bound into x and out of y, and zero, since a path through zero on
either side is an implied bound; a join takes the relations a side
stores, those between results that share a symbol, and those between a
value whose upper bound and one whose lower bound move the same way
between the sides (two counters stepped together), which are exactly the
ones the result’s own bounds do not imply; its closure runs over zero and
the symbols in stored relations. Equality compares what the zones answer,
not what they store. zlib’s units took 2.9 s instead of 12 s, with the
same ledgers.widenEffects): the integer results of one case become one interval
whose bounds that moved are dropped; what an unknown effect on every case
covers (every object reachable from its path, which the caller forgets)
is folded into it on both sides before the join (unknown effects, and
possible releases and moves, below its path; stores into the objects it
covers); and new objects are numbered by first appearance, so rounds that
differ only in the join’s numbering compare equal. Rounds had replaced
each summary instead, and mujs’s parser, compiler and runtime, one
component through js_throw, never settled: every summary in it was
incomplete, so every call into it applied the unknown-callee default.
A component that still does not settle after eight rounds is incomplete
as before, and a member its last round said never returns may return.do { fclose(stdout); } while (…) is a possible double free at the loop head).va_list (§5.1). va_start and va_copy initialise the va_list
they are given by reference.data[i] with i <= 10).release *p when result null and param 1 =0), and is otherwise possible. A result alternative carries the
parameter test every exit returning it passes, so a call whose argument
fails the test never gets it (xrealloc(p, 0) returns null). Pending cases
that the arguments already decide are applied at the call. Releases record
what the state knew of the releasing function’s unmodified integer
parameters (paramGuard), which keys a possible release that no exit
class separates (lossy), and which pointer locals of the body’s outermost
block, assigned only where they are declared, held a non-null value
(nonNullLocals): an exit that returns such a local is derived per
result class too, and on its null class the release did not happen
(m = malloc(n); if (m && p) free(p); return m; releases *p only
when the result is non-null).return *out != NULL, return --*r == 0) is derived per result class
too, the class refining the comparison’s operands. A store that holds a
new object on some classes and null on the others (its allocation failed
there) is one store whose object is absent on those classes (format 30
absent-on=<classes>): a caller’s test that selects them disowns it, and
the others make it non-null. Integer values of a store that differ per
class are joined into their hull. A callee with several result
alternatives, or a stored value it cannot describe, depends on its inputs
for the numeric contexts of §6.6.SymInfo::entryOf tells an entry value from a store).release *param0 offset 16), and the call reports an interior release of a
caller’s allocation there.unknown object (what an unknown callee left in a cell, an integer made a
pointer) is unresolved(unknown-callee), never proven; cells of memory an
unknown callee reached hold unknown values whatever the object’s kind.result->a).main’s locals, whose frame lasts until exit
(RFC 0030 §3.4).main’s argc is non-negative (C11 5.1.2.2.1).n=… a=… c=… m=… f=… g=…, functions and
globals by portable name: their own for external linkage, <source>#<name>
otherwise). UnitExports carries the contextRequests a unit makes and
the contextEffects it serves; the program database collects both. The
defining unit runs each requested context (at most 16 per callee) and
exports its summary; a context that makes arguments one object also runs
as an alias context there, reporting what it finds inside the callee. The
whole-program loop reruns the definers of unserved requests (once per
request), then the dependents of every unit whose exports that changes,
until the requests are served (at most 8 rounds). A request stands for as
long as a call makes it. A unit whose run asks a context a unit of the
program has yet to serve reports nothing from that run and publishes no
ledger: it reports once the context is served, or after the last round
with what is served, so its diagnostics are those of its last run. For
the same reason weavec --whole-program prints each unit’s summary line
from its last run, after all runs. At link, a unit the program view does
not otherwise re-analyse stays a serving unit: it runs again only if
another unit asks a context of it. A constant unsigned 64-bit argument
above INT64_MAX is carried by its bits and binds the parameter’s
interval.
A call through a function value names this unit’s functions by
declaration and another unit’s by portable name (SymInfo’s
foreignFunctions), applying the latter by their summaries; a slot
solution’s targets this unit does not declare are applied the same way.
Summaries describe a function value as function <names> (format 30
fn=), and a unit that only refers to another unit’s function imports it.
This replaces the old engine’s callback and memory context requests (RFCs
0014 and 0016).may, keyed by no
case, the callee always returns) whose value is known, to one cell or to
a range of elements whose bounds are constants at the call, that lies
outside the one object the argument points into (an exact, constant
extent) is a spatial violation of the call and an out-of-bounds error
at the argument, in RFC 0030 §7.5’s wording (“‘f’ requires N bytes behind
‘a’, which has M bytes”, “‘f’ requires ‘a’ before its start”, measured
from the object’s start); a call §7.5 already found gets no second
finding. A known value says the callee wrote every element of the range:
an element it skipped would hold its entry value, which a range of
unknown values allows. With contexts (§6.6, §7) this recovers what format
29’s extent requirements found at link: the context binds the call’s
constants, so its summary’s stores name the exact bytes written. An
unsigned 64-bit integer above INT64_MAX in a summary is carried as its
interval (format 30 range=<type>:<lo>-<hi>).n * sizeof *p)
records the mathematical product it is the reduction of (SymInfo’s
unwrapped); an allocation of it records the same on its extent
(Extent::unwrapped). The bytes are never more than that product, so an
access past it is a violation of an exact extent (p[n] after p = malloc(n * sizeof *p)), while an access below it is not proven by it.KindInference‘s, after its disqualifications, for records defined in
the unit’s main file (a header’s record is other units’ to make too, and
their stores would need §7.6’s link verification, A3, which is not
built). After the unit’s summaries and authoritative runs, Houdini runs
over them: each round assumes, per pointer field, the first standing
candidate at entry (its field’s kind becomes counted/sized over the
count field, exact); a checking run of every function that writes a
candidate’s d or f (or initialises its record) examines, at each call
and at each exit, the objects of the record it hands out (arguments, and
every object but its locals at an exit). A candidate holds of an object
when d is null, or points to the start of one object whose exact extent
is (f + c) * unit (unit the element size for count, 1 for bytes),
or is the size type’s reduction of it (above: what f * sizeof *d
computes in C). Where d holds its entry value only the assumed
candidate is decided, and only when f changed; another over a changed
f is refuted. A candidate a stored d meets is witnessed. Refuted
candidates are dropped and the round repeats, at most four times (still
refuting after that: none stands); a candidate no function witnessed is
dropped too (it would only replace the kinds’ default). The functions
that read a standing invariant’s d are analysed once more with it
(their rows replace the first run’s; a diagnostic the first run made
links to the new rows); summaries stay what callers used. Where the
count’s range cannot rule out the wrap, a load of d gets an extent whose
bytes are a symbol computed as f * unit in C, with the product as its
unwrapped bound: v->items[v->cap] is a violation, v->items[i] under
i < v->cap is checked, not proven.OwningCycle fact is a SharedOwners row
with a cycle flag, whose detail reads “closes an owning cycle”.--whole-program, every address-taken function of its type: the unit’s
own and the database’s joined candidate summary. In a unit alone it keeps
the unknown-callee default. The unit’s call graph orders slot-resolved
targets before their callers.sizeof
and extents use the captured values. A dimension that may be zero or
negative gives no storage to prove an access in; one that is for every
value is the declaration’s invalid-integer-operation. A byte size that
may wrap size_t is no extent (and no sizeof check). A subscript of a
multi-dimensional array is bounded by its own dimension; an index past it
is a violation whatever the storage..#16), not its index: the engine names cells by
offset, and an offset decodes against any view of the object.EffectsIO spells the summary as one item
per line: returns, incomplete, effect <kind> <path> when=<case>
with family=, may, lossy, offset=, elements=; store <path> when=<case> [may] [elements=...] :: <value>; result classes=<c,...> [param=<i>=0|<i>!=0] :: <value>; nonnull-on, reads, writes. Paths
are p<i>, g<i> or r followed by *, .<name> and [<name>].
Every field round-trips. The database joins several definitions of a name
(and the candidates of a type) with joinEffects, and renumbers globals
by name; an effect through a global the importing unit does not declare
makes the summary incomplete there (RFC 0030 §5.5).effects; summary, contexts, unknownIndirect,
sizedFields, sizedFieldLoads and interfaces are gone. The link
step’s declaration verification reads the format-30 summary.IntegerSupport.h is kept as the engine’s
EngineIntegers.h (target integer types for the engine), and the checked
arithmetic of CheckedInteger moved into Integer.cpp; the signature
annotations of Summaries.h moved to Annotations.h. Everything else §1
lists is deleted, with SummarySteps and Place replaced by Path.h.SplitDispatchEdges
(lib/Frontend/DispatchEdges.cpp), registered for optimised builds whose
checks are emitted.p == q, p != q),
and a join keeps those both sides decided alike. A release records the
comparisons of the function’s unmodified pointer parameters on its path,
and the summary keys the effect by one of them: <case> gains and param <i> ==|!= param <j> (spelled <classes>:<param test>:<i>==<j> in format
30). A call skips the effect when its arguments decide the comparison the
other way (a remembered comparison, a null argument against a non-null
one, or targets that cannot overlap by §4.5), and takes it as possible
when they do not decide it. This is RFC 0014’s release_same(p, q) guard
(evaluation/rfc0014-pointer-guard-good.c).callback for an indirect call through a slot with no known
target, for what the arguments reach as for the call (RFC 0030 §9.3).if (!g), if (b->buf == NULL)), by cell, and a join keeps those both sides made alike.
A join where one side stored a cell under a test and the other took the
opposite test and left the entry value marks the cell as stored exactly
where the test holds; an allocation one side made under a test the other
side refuted exists exactly where it holds. A summary keys an effect or a
store by such a test, <case> gaining and entry <path> =0|!=0
(spelled :E<path>=0 in format 30), and a call applies it by the value
it holds at <path>: skipped when that value decides the test the other
way, possible when it does not decide it, where the new object of such a
store exists exactly where the caller’s own entry value tests as the
callee’s did. An entry object exists where its holder’s entry value is
not null; a store through a pointer to two objects of which each path has
exactly one (the entry object and the object made where that value was
null) is strong, and a load through it reads one value while both cells
are unchanged, so a test of it holds for the next load. This is if (!g) g = malloc(…) (HeapState.LazyPublicationKeepsItsEntryGuardAcrossTheCall).if (c) p = malloc(n); … if (!c) return;). An object one path leaves live that the
join ahead no longer shows live (it was released on the other paths) is
leaked on that path, reported at the branch that takes it, when the edge
reaches the exit through blocks with no statements (switch without a
default, if (c) free(p); at the end of a body); lifetimes that end in
a block with no statements are checked there, and a leak found there is
reported at the end of the scope.return), the summary stores the new
value as a possible store, which the caller applies weakly, instead of
describing the join as unknown. A vec_push that may realloc its
buffer thus leaves the caller’s buffer pointer either the old buffer
(possibly moved) or the new one, and an element pointer kept across the
call is reported (soundness/44_vector_element_ptr_bug.c).noreturn (_Noreturn, __attribute__((noreturn))) ends the
path whatever its summary says, as a call whose summary never returns
does: a parser’s error routine that longjmps out of a half-built frame
joins nothing at the exit.unknown effects name the callee’s entry state, so the caller
resolves every one before applying any (applying unknown *p first
would forget the cell *p->next is read through). An entry object an
unknown callee may have released and that no root reaches at the exit is
a dead copy; while no live object has its key it still names the entry
object, and the bytes the callee rewrote are an unknown store at its
path. Without the store, a caller kept the old contents of a record whose
fields the callee reset before passing it on (Lua’s close_func after
leaveblock).
An unknown effect on *p reaches, in the caller, every object the
caller’s memory reaches from *p, as a direct call to unknown code does:
the callee’s summary names only the objects it materialised, and the code
it could not see had the rest (semantics/temporal/unknown-effect-reaches-frame.c).while (l++ < width) in printf’s _vsnprintf, climbed one small
integer a round, and again each time an enclosing loop entered it anew.) A function is
over budget when a loop head changes more than 64 times (a join that adds
nothing, such as another back edge of the same round, is no change), or
is joined more than 256 times; a head many edges reach (an interpreter’s
dispatch, an edge per opcode) may change twice and be joined 64 times per
edge, and a loop entered again from outside it (with what a round of an
enclosing loop changed) counts afresh. It is also over budget when a
join leaves a block’s state with more than 2,048 symbols, each of which
every later join pairs: sqlite’s sqlite3VdbeExec converged once its
dispatch settled (below) with states of 4,677 symbols, in 90 to 180 s a
run, where the next largest state in the corpus holds under 1,000. A local whose scope does not hold
a block, that nothing after it names and whose address is not taken,
leaves that block’s state (liveness now ends at a declaration and sees
the references an element holds): the opcode bodies’ locals had piled up
at Lua’s dispatch head, which never settled; luaV_execute is no longer
over budget. The worklist takes blocks in reverse post-order, except that
a loop head a back edge changed waits until no block of its natural loop
is pending, so it takes in every back edge of a round before the next
round starts (Bourdoncle’s order for reducible loops): a computed-goto
dispatch, which every opcode’s end reaches, had restarted the round at
each of its 80 back edges, re-running luaV_execute 244 times through
the dispatch; it now runs about as many rounds as the head changes. A
bound against zero stops at the next program constant; a bound between
two symbols stops only at -1, 0 or 1 (a relation climbing one constant a
round kept zlib’s crc32_z from converging). Result symbols are numbered
in the same order on every join: the objects’ symbols first, then the
values of expressions one side still holds, so two states that differ
only in such a value still compare equal. A loop head’s new state that
differs from the old only in how its symbols are numbered (a join numbers
its results in pairing order) is no change: the two are compared under a
bijection of their symbols, conservatively (a field the comparison does
not follow must be equal as it is).read into part of a record) forgets that range only: the object keeps
forgotten ranges beside the whole-object havocked, and an unwritten
cell reads as unknown only inside one. A range forgotten on some paths
only (mayForgotten, and a join of a path that forgot it with one that
did not) reads as the cell’s value otherwise merged with an unknown
one. Summaries say which bytes of an entry object were rewritten
(store *p bytes 24..32 := unknown, format 30’s bytes=), possibly
(may) unless every exit rewrote them on every path; the caller forgets
exactly those bytes, before the summary’s other stores, and on a
possible rewrite keeps each cell’s value beside an unknown one, so a
pointer it left there still reaches its object
(semantics/temporal/possible-rewrite-stays-possible.c).realloc of zero bytes (RFC 0030 §8.2, §11). RFC 0030 releases the
argument of realloc on the null class when the size may be zero,
because glibc’s realloc(p, 0) frees p and returns null. A build with
zero-initialisation (the default, and what the analysis models unless
-fno-weavec-zero-init or --no-zero-init says otherwise) calls
realloc through __weavec_realloc_zero, which asks for one byte
instead of none, so there the null class always keeps the argument and
the release does not arise. Without zero-initialisation RFC 0030’s rule
stands. A buffer wrapper’s failure path (if (!new) return; after
realloc(ab->b, ab->len + len)) thus keeps a live buffer, and no
boundary sees a pointer that may be gone (linenoise’s abAppend).SymInfo:: entryOrigins), and publishes their classes with the proof
(LedgerAdapter::reliesOn). The propagation downgrades a proven facet
whose operand is spelled from a broken class, as before, or whose value
came from one. A broken field class struct s.buf no longer breaks the
record class struct s: that rule reached b[0] after char *b = o->buf only by also downgrading every o->len in the program, a third of
zlib’s temporal facets for one ZFREE(strm, strm->state) whose own fields
held the buffers just freed. Probe 02d’s bug site is reached through the
field class directly; soundness/02g_uaf_heap_field_copied_helper_bug.c
covers the copies.stdout, stdin, stderr) are the
exception: the objects they point to are still exposed (the stream may be
closed), but the name keeps its value, so the stream a later round reads
from stdout is the one an earlier round closed. Without it, the reload
materialised a fresh copy of the entry object and minigzip’s repeated
fclose(stdout) (RFC 0030 G9) went unreported
(semantics/library/stdout-closed-twice.c).memset(p, 0, sizeof *p)
on a parameter’s record leaves an object whose unwritten cells read as
zero, which a summary did not describe, so the caller kept the record’s
old contents (a pointer the function had just freed before clearing it,
cJSON’s test reset). The summary now stores zero (null for a pointer)
into each scalar member of the record’s type that the function wrote no
other value into.d.#4, the high word of a double a
union also holds as integers) by that offset; the caller no longer takes
the member’s type for the store’s, which made the store run past the
object (jansson’s dtoa; semantics/objects/union-high-word-store.c).
Past the end of a scalar the offset still names another element of its
type (b[7] through a char *).reads
and writes, which the engine now fills: the entry objects whose cells
it loaded or stored); a callee that reads it first makes that read the
first invalid operation, a use after free, as before. The callee is every
function the call may reach (a hook slot’s internal_free and free):
some that may release it make the release possible, all that do make it
certain (semantics/temporal/free-then-guarded-wrapper.c,
semantics/slots/hook-freed-twice.c).free(s - hdr_size(s[-1])), hiredis’s sdsfree),
or at different offsets on different exits, was summarised at offset 0,
so the caller released its argument itself and reported a definite (and
false) invalid-release when that pointed past the object’s start. Such a
release is written offset=? in format 30 (PathEffect::anyOffset); the
caller releases the same objects at an offset it does not know either,
which leaves the release’s validity possible, not definite
(semantics/library/release-at-unknown-offset.c).sds string after its header) was still summarised
as zero-filled, so the caller read the header the callee wrote as zeros.
Its cells now read as unknown in the caller, as those of a new object
below another one’s already did.m->sub[i] may be
counted from the start of m->sub[i + 1]; a summary names its store as
param1->sub[*].ep with the element range shifted back by one, where it
used to drop it (mujs’s Resub). A store’s extent past the caller’s
object is measured to the end of the last element’s cell, not the whole
stride (semantics/objects/member-array-record-fields.c).sub->sub[i].sp = NULL through a
pointer to the caller’s record or a local one) as the entry path’s value
maybe-null; a value only maybe-null at entry, the path’s value alone. A
join that takes in a null pointer beside another value marks the result
(SymInfo::nullJoined), and only such a value is described maybe-null.
The call now applies both what a path value’s description says: its
offset, which a store used to drop (s + 1 stored as s), and its
maybe-null, a join of the value with null, which it used to ignore (the
caller’s unassigned elements stayed unassigned, a definite and false
use-of-uninitialized). A path result keeps the caller’s nullness of the
value, where the callee’s view of an entry value’s nullness used to make
it maybe-null (semantics/objects/optional-out-elements.c). A cell
that holds its own entry value or a null the function stored (if (errorp) *errorp = NULL) is a store, where “still holding its entry
value” made it none (semantics/objects/guarded-null-out.c).sub[*].sp, 8 of each 16 bytes), where
it used to use position 0 (the field ep’s), reading what the elements
held first; at an offset the call does not know, the object’s cells are
forgotten.setjmp (RFC 0030 §5.4). In a function that calls a
returns-twice function, a longjmp may return to it after stores the
path through the first return does not see, so a local no path assigned
reads as possibly unassigned there, not certainly: no definite
use-of-uninitialized (mujs’s js_try handlers, which free what the
protected code allocated; semantics/ledger/setjmp-assigned-after.c).SymInfo::rawSome, the
summary’s raw-some). Using, releasing or passing it outside an unsafe
region is no definite unsafe-operation, which made every allocation in
the library an error; its facets are unresolved(raw-cast). Loads
through it and joins keep the mark; only such a call makes it, so a raw
value a join in the function makes is still an error (RFC 0004’s
“rawness joins as may be raw”;
semantics/slots/hook-raw-on-some-targets.c).ObjectKey::expression); code that asked every local for
its variable read an expression as a declaration.0 != settings->on_begin(p)). A join numbers its
result’s symbols afresh, so such an operand could name another value
afterwards, even the comparison’s own result, whose condition then named
itself (http-parser’s whole program recursed until the stack ran out).
Such a join keeps the number of every symbol both copies still hold
from the state before the call, and numbers the rest past it
(Heap::join’s keepBelow); refining a condition also stops at one it
is already refining.out-of-bounds). A run that applies an unknown
callee, or a summary that is incomplete or says this, now marks its
summary unknown-globals (format 30, FunctionEffects::unknownGlobals),
and a call of such a summary, or of an incomplete one, forgets what the
caller’s globals hold as an unknown call does
(semantics/boundary/global-written-by-unknown-code.c). So does a
summary imported into a unit that cannot name a global it stores into
(another unit’s static counter a callback increments), where the store
used to be dropped without a trace. The flag names no global: a caller
forgets all of its own, which costs http-parser’s test a third of its
proven facets (Unresolved questions).Transfer::contextSummary): a context run of a
large function costs what its own run did, for each context
(http-parser’s http_parser_execute, whose sixteen contexts took more
than twenty minutes).corpus-gate.py --full --held-out, the case runner and the scripts the
gates name, on the reference machine.
--asan and under --checks verify; no proven facet trapped in any corpus or held-out test suite
built in verify mode); G2 (481 of 481); G3 (evaluation 76 of 76, pairs
24 of 24, recall 67 of 67 pins, engine 143 of 143 pins, soundness 137
of 137 with 75 probes reported, injections 30 of 31); G4’s errors (0
definite errors on the original configs, zlib’s fclose(stdout)
reported) and traps (0 in every test suite); G5 (no definite error
triaged false; every held-out project builds and passes its tests with
no trap, except sqlite, whose build stops at the three definite
unsafe-operation errors on pointers made from integers, triaged true
under RFC 0004); G7 (153 units, identical in all three
configurations); G8 (Lua 1.08, zlib 1.01, cJSON 1.15); G9 (Lua’s whole
program, 114 s); G10 (zlib make -j8, 1.7 s); G11 (4 of 9,588
functions over budget); G12’s single units (sqlite3.c 486 s and
2.3 GB, mujs/one.c 12 s); G13 (in retired instructions against
v0.11.0’s binary, Lua 1.02, zlib 0.67, jansson 0.97, and the smaller
configs within the gate’s second of slack: cJSON 1.27, printf 1.49);
H2; H3’s build and pages.unknown-callee, 5,897 of 7,249 of them Lua’s: luaD_precall calls a
lua_CFunction through a slot of about two hundred targets, past
MaxCallTargets, and any of them may run the collector, which may
release whatever the state reaches (without Lua the share is 0.23).
95% of the unresolved spatial facets are unknown-extent: pointers
loaded from fields whose kind inference proves no element count, and
strings walked by pointer. sqlite holds 48,370 of the held-out
configs’ 86,601 temporal facets, at 0.60: its allocator, mutexes and
VFS are hooks in sqlite3GlobalConfig, open slots sqlite3_config
sets. G12’s build ratios: bzip2 11.0, http-parser 31.6, lz4 13.0,
mujs 46.7 and utf8proc 9.2 against the reference compiler, limit 8
(hiredis 1.9, inih 4.8, libyaml 2.9, miniz 2.9, tinyexpr 7.6): the
link step analyses the program again for every executable it links,
and the analysis of one unit alone exceeds the bound for http-parser
(http_parser.c, 3.5 s against a whole build of 0.6 s) and mujs.
H1 is measured by CI. G5’s traps depend on the fuzzer’s seed in
one place: lz4’s frametest makes corrupt frames, and on one of them
LZ4_memcpy_using_offset_base copies with an offset of 0,
memcpy(dst, dst, 2), whose operands overlap, which C leaves undefined
(lz4’s own comment says an offset of 0 happens in testing). The
disjointness requirement there is checked, not proven, and its check
trapped in the verify-mode run; the trap-mode run’s seed did not reach
it. It is a true report: a trap of the kind G5’s exception names for
definite errors, which this RFC records as true here.
gates.G10 of the corpus manifest; RFC 0030 G10 is amended alike).expected.json
are the ratchet; over the held-out configs together the temporal share
is at most 0.50 (was 0.35), on the program ledgers where a
whole-program analysis exists. The targets of 0.35 and 0.30 over the
original configs and 0.35 over the held-out ones go to RFC 0032.sqlite3.c and mujs/one.c) stay. The bound of 8 goes to RFC 0033.
G5 counts a trap of a checked facet that reports a defect the source
shows as it counts a definite error triaged true. With these, G1–G13 and
H2–H3 hold on the final tree (H1 is CI’s), and this RFC and RFC 0030 are
Implemented.isDupColumn context: ten thousand ranges of
one object, eight gigabytes). A join’s result now keeps each position’s
newest four, and evictions go in one pass (Heap::trimSegments,
evictSegments); sqlite3.c takes 850 s and 1.4 GB.free_tree(t->left) makes left owning), where only a library
release or an ownership contract counted.semantics/objects/tree-destructor.c).p->f, where
f is an owning slot, records p among its derivation, which D6 reads:
it was never set before.HeapState::nullFollows, from each result to the pointer its chain
started from): a dereference or a test that decides one non-null decides
the others, even when the value the chain started from has no pointer
type (an untyped union cell, Lua’s ci->u.l.savedpc). *(q++) decides
the incremented q too, so each fetch of an interpreter’s pc after the
first is proven non-null; Lua’s luaV_execute had a null check at every
opcode’s fetch. The links are not kept by joins, where each side’s
values are paired anew. A pointer made by adding an amount that is not
zero (a constant, or a value whose bounds exclude zero) is non-null: were
the pointer null the arithmetic would be undefined, and its result is not
the null pointer, so a check of it could not fail (Lua’s base = ci->func.p + 1, from which every register pointer is made).unknown-globals) names no effect on one global inside its
component, beyond releases and moves: its callers forget every global
anyway (the C library’s own excepted, whose effects stay), and those
effects climbed one call edge a round, so the component of 1,392
functions never settled in eight rounds (1,388 changed in the first,
265 still in the eighth).HeapState::exprs) only while
some path from it reads them before evaluating them again: a backward
liveness over the CFG, whose reads are the expressions in a block’s
elements, its expression terminator and its condition, and whose kills are
the expressions it evaluates and, for a conditional’s value, its branch
and the blocks of its arms. A conditional reads neither its condition nor
its arms (its value is the one its arm recorded under it), so they are no
longer carried for it. Its arm is found with its parentheses stripped, as
the CFG evaluates it: an arm in parentheses (c ? (x = i, 5) : 0, Lua’s
tonumberns) had recorded nothing, and the operator’s value on that path
was the other arm’s from an earlier iteration, a false proof
(soundness/conditional-arm-in-parens_bug.c); the branch now forgets the
operator’s value, so an arm that records none reads as unknown. Lua’s
dispatch head carried about 150 such values, each a symbol every join
paired.lib/Analysis/Engine*.{h,cpp} (13,795 after S6: the S7 fixes above) and
64,367 under lib/, include/ and tools/ (LibrarySpec.txt excluded;
56,610 after S6), within this RFC’s ceiling of 80,000. The hygiene gate’s
budgets become 20,000 and 64,500, the measurement rounded up to a multiple
of 500; they are ratchets, raised only by an amendment that records a new
measurement. Measured again once the branch was formatted with
clang-format 23 (the CI’s; the S0–S7 checkpoints were not) and the last
S7 fixes above: 19,793 and 64,609 lines, so the second budget becomes
65,000; with the sparse zone and recursive widening, 19,944 and 65,120,
so 65,500; with the ranges a join trims, owning slots through functions
and owners below the k-limit, 20,040 and 65,276, so the engine’s budget
becomes 20,500; with the iteration order, the carried values’ liveness
and the cost bounds of this round, 20,373 and 65,872, so the second
becomes 66,000; made clean under CI’s clang-tidy (braces, split
declarations, suppressions with their reasons), 20,535 and 66,102, so
the budgets become 21,000 and 66,500.Drawbacks
Section titled “Drawbacks”
FunctionDataflow encodes
hundreds of decisions made against cases, many of them described only in
commit history. The test tree (410 cases, the lit pins, the corpus
ratchet) is the specification this RFC relies on, and whatever it does not
pin can regress silently in precision (never in soundness, by §4.7).Alternatives
Section titled “Alternatives”
FunctionDataflow. A lazy fallback at lookup (check the
mirrors of ancestors) fixes the five probes. It does not fix the class:
every tracker keyed by path has its own copy of the mirroring rules, and
the next false proof is in whichever one was not patched. It also leaves
the cost and the false errors of Motivation.Prior art
Section titled “Prior art”
heap recent/heap old split that allows strong updates on the most recent
allocation.Box trees are the owner forest of A3; the forest is
what makes a destructor loop provable without shape invariants.-fbounds-safety: unchanged from RFC 0030; the
kinds and checks this engine feeds are theirs.Unresolved questions
Section titled “Unresolved questions”
while (p) { next = p->next; free(p); p = next; }) needs, at the loop head, one object that
stands for “the rest of the list” and is disjoint from every node already
released. The focus objects of §4.6 give one per join, but a release of the
focus weakens its candidates, so the released nodes stay possible
overlaps and the list destructors of semantics/objects/ stay
unresolved(may-alias-released) (the tree destructor is proven, D3 below
the k-limit). A value-level validity fact (a pointer that points to no
object released since it was obtained, invalidated at each release) was
tried and does not suffice: the focus object’s cells join the released
node’s stale values with the live one’s. A list-segment abstraction (the
candidates of a focus that the other side released and no root reaches are
absorbed into it) is the candidate fix.unknown-globals makes a caller
forget every global it holds. A summary that named the globals another
unit’s code writes (by their portable names, as contexts do) would keep
the rest, but needs a unit to name globals it does not declare.Future work
Section titled “Future work”
unresolved(unknown-extent) facets become checks
against the runtime extent and unresolved temporal facets become liveness
checks (with a quarantine), on top of proofs this engine makes sound.weavec.toml,
weavec suggest --apply, relocatable installs.lua_CFunction, any of which may run the collector), hooks in
configuration objects that code outside the program may set (sqlite’s
allocator, mutexes and VFS; cJSON’s), and extents nothing in the code
states. RFC 0032’s runtime extents and liveness checks turn the last
into checks; the first two need a model of hooks (a joined summary per
slot, or declared hook contracts).weavec-cc
build within 8 times the reference compiler’s CPU; 2 to 47 times at its
close). The link step analyses the program again for every executable it
links: it should reuse what the compile step decided for the units whose
program facts did not change, and keep results across links of the same
objects (with RFC 0033’s records in object sections).