Development history before 0.1.0
This historical snapshot preserves the hand-written notes from before the
first public release, including implementation details and migration notes.
It is not maintained for subsequent releases. Python Semantic Release creates
CHANGELOG.md on the first public release and regenerates it thereafter.
All notable changes to WeaveC are documented here. The format is based on Keep a Changelog and the project follows Semantic Versioning once it reaches 1.0.
Release automation
Section titled “Release automation”- Publish versioned source archives and SHA-256 checksums to GitHub Releases after main passes CI. Conventional Commits select 0.x versions, stamp the CMake version and retain the hand-written changelog and migration notes.
Scalable checked analysis (RFC 0020)
Section titled “Scalable checked analysis (RFC 0020)”- Reuse context summaries according to observed function and global-fact dependencies, including missing callees and nested cache hits.
- Retain parsed translation units and reusable CFG, scope and liveness data during whole-program analysis and compiler source replay. Bound ordinary runs without persistent reuse to one retained AST, and share owned call snapshots across CFG transfers. Release stale working databases and duplicate completed exports before constructing their replacements. Join spatial and alias facts directly and consume settled diagnostic-pass states while preserving the final exit state and analysis dumps. Schedule checked CFG work in reverse postorder with a reusable block order; preserve FIFO convergence and resource use throughout ordinary analysis.
- Share immutable checked obligation entries and ledger snapshots on copy and across identical completed contexts through bounded weak indexes; separate semantic convergence from explanation wording and call routes.
- Add optional
--analysis-cacheand--analysis-statsoptions, with matching compiler spellings. Settled checkpoints validate preprocessing, input bytes, commands, tool identity and imported facts before replaying diagnostics. Shared checkpoint tables preserve contracts too large for expanded transport; per-function statistics and partial snapshots expose unfinished work. - Add optional compact checked report version 3 and a compatible expansion script. Expanded version 2 remains the default.
- Bind compiler objects to their preprocessing outcomes in sidecar format 16, closing stale-object validation when conditional includes become available. Rebuild older sidecars; Core summary format 15 remains unchanged.
- Add frozen cache equivalence, invalidation, corruption, compiler-object and compact-explanation regression cases.
- Fix summary parsing of widened
Insideoffsets (@~) in stores, returned values and heap descriptions, preserving facts in large checkpoints. - Preserve reachable checks after diverging loop cursors: checked nullness and branch facts require proven aliases, rather than possible equality on an earlier iteration.
- Encode imported-fact keys losslessly, including spaced function-pointer type spellings and source paths, so independent units remain cacheable.
Practical checked memory contracts (RFC 0019)
Section titled “Practical checked memory contracts (RFC 0019)”- Infer initialized output intervals through nested fields, returned heap objects and output parameters, qualified by guards and returning outcomes. Preserve call-entry numeric values and initialization when helpers overwrite inputs; discard pending facts when their dependencies change.
- Prove complete supported fill/copy loops and disjoint slices of one object. Keep pointer holders separate from pointee storage and retain unaffected evidence across complete helper calls.
- Add checked string and positive-size reallocation models, initialized-prefix preservation, and zero-byte termination witnesses. Library trust remains explicit; allocation growth does not initialize the added tail.
- Reuse bounded caller contexts for exact checked scalar inputs and fields. Keep recursive explanations stable without raising semantic analysis limits.
- Transport conditional contracts and numeric outputs in summary/sidecar format 15 and checked JSON version 2. Rebuild objects with older sidecars.
- Keep optional private-global output facts inside their translation unit, while rejecting lost entry requirements or premises of public outputs.
- Add 32 frozen source cases, ten whole-program and compiler-object cases, and pinned Jansson buffer/UTF interface evaluations with adversarial callers.
- Publish the validation report with repeated ordinary cost measurements and remaining whole-project checked limitations.
Checked code (RFC 0018)
Section titled “Checked code (RFC 0018)”- Keep propagated call provenance bounded without recursively escaping identities.
- Require writable storage for direct and composed writes, rejecting writes to
literals and cast-away
constobjects. - Add opt-in function and module selection, separate sufficient safety contracts, per-operation proof accounting and must-initialized memory facts.
- Enforce callee contracts, including possible accesses in counted loops with early exits. Keep unknown bounds, uninitialized reads and unsupported semantics visible as checked failures.
- Add
WEAVEC_CHECKED,--checked,--checked-function,--checked-report, and corresponding-fweavec-*compiler options. Diagnostic demotion does not change checked acceptance. JSON reports record selection, requirements, unresolved operations, and transitive trust. - Transport checked contracts in summary/sidecar format 14. Bind compiler sidecars to object contents, source/header inputs and recorded commands; replay deferred external dependencies before linking.
-
C integer semantics and compositional spatial checking (RFC 0017): target widths through 64 bits, promotions, narrowing and
_Boolconversions, full-width unsigned values, mixed-sign comparisons and modular unsigned arithmetic feed scalar guards, ownership effects, selected elements and sizes. -
invalid-integer-operation(error) for definitely invalid supported arithmetic and nonpositive VLA dimensions. Possibly invalid arithmetic retains conservative facts; it cannot justify discarding a reachable path. -
Bounded symbolic size expressions, allocation-time operand snapshots, checked add/subtract/multiply builtins and guarded multiplication, numeric returns and output values, and conditional access intervals through summaries. Supported unit-stride loops can export
min(n, cap)bounds; early-exit and other unsupported loops do not infer must-requirements. -
Call-entry numeric snapshots preserve allocation sizes and typed guards when a callee overwrites their input counts, including out-parameters and global fields. Integer dereferences and address-taking retain stored numeric values. Numeric output paths that alias the same caller cell are joined; contextual summaries retain the callee’s write order.
-
VLA declaration-time dimensions and
sizeofvalues, allocated flexible-array tail extents, and counted-field inference that retains the C multiplication type.callocandreallocarrayretain checked-product failure semantics. -
Spatial
proven,violationandunresolvedcounts and unresolved reasons in--dump-analysis, independent of diagnostic suppression. Unsupported numeric representations and interface projections expose incomplete coverage. -
Summary and sidecar format 13 for typed facts, expressions, numeric outputs and guarded access intervals. Rebuild objects carrying older sidecars. No new annotation spellings or verification mode are introduced.
-
Twelve separate bug/clean regression pairs and a hardened recall runner that rejects process failures even after expected reports were printed.
-
An RFC 0017 validation report and reproducible corpus results: 900/900 tests pass normally and under ASan/UBSan, original fixed detection improves from 42/44 to 44/44 with 32/32 clean cases, and recall remains 67/67. Three runs per binary show median corpus time growing from 141 to 336 seconds and peak memory growing 19%, beyond the RFC targets. Three new Jansson false positives and incomplete coverage are documented.
-
Monotone recursive summary joins prevent typed guard projection from oscillating between summaries. Packed numeric-write sets retain the same facts with less state-copy overhead. Abstract range endpoints alone no longer fabricate reachable out-of-bounds witnesses.
-
Compositional call checking (RFC 0016): helpers are checked under the caller’s pointer, storage and reference-share relationships, retaining statement order through aliased parameters, fields, globals and selected elements. Release-before-use and repeated releases are diagnosed inside the helper; write-before-release and supported replacement remain clean.
-
Bounded memory contexts combined with actual callback targets, entry facts, final summaries, nested call notes and unsafe-reporting state. Contexts travel across translation units and compiler link analysis, including annotated definitions and locally complete object files.
-
Summary and sidecar format 12, with strict context validation and global remapping. Rebuild objects carrying earlier sidecars. Unsupported context projections and exhausted limits expose
analysis-incompletecoverage. -
Twelve additional fixed bug/clean pairs and Core, checker, whole-program and compiler regression tests. The RFC 0016 validation report records the fixed evaluation and the full diagnostic changes on the pinned corpus.
-
Array and container ownership (RFC 0015): selected pointer/record cells, stable scalar index snapshots, independent initialization and nullness, nested arrays, callback cells and retained-share copies.
-
Simultaneous complete array copies and overlapping moves, sparse symbolic range snapshots, final range summaries and returned containers. Reallocation preserves child pointer values while invalidating the original storage.
-
Proved contiguous cleanup and simple fill loops, with bounded initialization and release intervals. Unsupported selections and range compositions expose
analysis-incompletewithout discarding known release history. -
Summary and sidecar format 11 for selected paths, range copies, fills and releases. Rebuild objects carrying earlier sidecars.
-
Twelve additional fixed bug/clean pairs, exact array diagnostics, whole-program and compiler-sidecar integration tests, and a corpus manifest with pinned Lua, Jansson, linenoise, cJSON and log.c revisions.
-
Pointer identity and precise call effects (RFC 0014): bounded sets of actual callback targets flow through assignments, fields, copies, returns and out-parameters. Callback helpers are specialized with their actual targets while retaining the body’s userdata associations and statement ordering. Unknown callbacks remain checking boundaries even when unrelated functions of the same type exist in the program.
-
Pointer equality and inequality predicates on ownership effects, including argument snapshots and cross-file summaries.
-
Complete pointer and compatible record copies through
memcpyandmemmove, including pointer aliases, callback targets and reachable state. -
Record-layout metadata on summary paths and a stable
analysis-incompletewarning for unsupported memory copies, incompatible views and exhausted analysis limits. Dumps show callback targets, binding contexts and coverage. -
Summary and sidecar format 10, including callback contexts and global target values. Rebuild object files carrying older sidecars.
-
Paired RFC 0014 evaluation cases and a pinned corpus subset. The corpus runner records process failures, timeouts and optional peak memory, rejects empty runs and source-revision mismatches, and does not update a baseline from a failed analysis.
-
Analysis unit-test fixtures now reject Clang parse errors. Invalid existing fixtures were corrected; macOS SDK discovery no longer leaks Xcode startup messages into checker output.
-
Interprocedural heap postconditions (RFC 0013): constructors preserve owned children, argument aliases, shared children, self-links, bounds, null/raw fields and string facts through pointer returns, record returns and out-parameters. The same facts cross translation units and compiler sidecars; freeing a returned container now reports its unreleased children.
-
Allocation-time size identities: reassignment of a size variable no longer erases a known constant extent; bounded symbolic snapshots preserve useful relations to copies of the original count.
-
Final output values are distinguished from intermediate stores and incoming pointer values. Replacement invalidates old aliases, failed replacements restore the incoming pointer and its bounds, and pointer extraction retains the removed value. Lazy initialization preserves an existing object’s state.
-
A fixed evaluation set, including known misses, with separate counts for detected bugs, clean programs, unexpected reports, parse failures, tool failures and timeouts (
scripts/evaluate.py, part of CTest). -
Heap coverage in
--dump-analysis: descriptions have explicit depth and alternative limits; truncated descriptions are printed asincomplete. These fields were introduced in format 9; the current format is 13. -
Reviewed corpus measurements for RFC 0013, including the substantial increase in false positives on Lua’s GC and broad callback candidate sets. The corpus notes record the precision cost alongside the new capabilities.
-
Initial project scaffolding: CMake build with presets, LLVM/Clang discovery, strict warnings, sanitizer and LTO options, install/export rules and CPack.
-
weavec::Core: Clang-independent ownership lattice (OwnershipKind), lifetime constraints, loan tracking (BorrowState), move tracking (MoveTracker) and a frontend-neutral diagnostics interface. -
weavec::Analysis: annotation recognition and a first path-insensitive local ownership checker reportinguse-after-free,double-free,invalid-annotationand (opt-in)annotation-required. -
weavec::Frontend: ClangASTFrontendAction, diagnostics bridging to Clang’sDiagnosticsEngine, resource-directory discovery. -
weaveccommand-line tool built on libTooling (weavec file.c -- <flags>), with--report-unannotatedand--analyze-headers. -
weavec.hannotation header:WEAVEC_OWNED,WEAVEC_BORROWED,WEAVEC_MUT,WEAVEC_UNSAFE,WEAVEC_ENABLED. -
GoogleTest unit tests and lit/FileCheck integration tests.
-
GitHub Actions CI (Linux ASan/UBSan + Release, macOS Release, clang-format, cmake-format, clang-tidy, CodeQL), Dependabot, issue and PR templates.
-
Project documentation: architecture, annotations reference, developer guide, roadmap.
-
RFC process for changes to the model, checker rules, annotations and diagnostics (
docs/rfcs/), with RFC 0001 (ownership model), RFC 0002 (sound intra-procedural checking), RFC 0003 (signature inference) and RFC 0004 (unsafe boundaries). -
Sound intra-procedural checking (RFC 0002): a forward dataflow over
clang::CFGreplaces the path-insensitive AST walk, so loops,switchfall-through,gotoand short-circuit operands are analysed on every path and each problem is reported once. -
Pointer copies are tracked as aliases (
core::AliasRelation): freeing through one name frees every name, and the note says which (freed here (through 'q')). -
Structured places:
s.f,p->f,*pp, nested field paths, and one summary place per array (a[*]). -
Allocator and release recognition (
malloc,calloc,realloc,strdup,strndup,aligned_alloc,free,WEAVEC_OWNEDreturns/parameters), with thereallocfailure idiom (if (!q) free(p)) accepted. -
New diagnostics:
use-after-move(owned pointer passed to aWEAVEC_OWNEDparameter or torealloc, then used),conflicting-borrow(two live borrows that conflict, or writing/freeing/moving a borrowed object) andlifetime-too-short(a pointer to a local stored somewhere that outlives it, or returned). -
Borrows from
&x,&s->f, array decay andWEAVEC_BORROWED/WEAVEC_MUTarguments; loans are mutable unless the pointer’s pointee isconst. -
weavec --dump-analysisprints the inferred places, lifetimes and exit state of every analysed function. -
Signature inference (RFC 0003): every function definition in a translation unit gets a
core::FunctionSummary(effects on parameters, paths under them and globals; stores into caller-visible memory; return-value provenance), inferred bottom-up over the call graph with a fixpoint inside recursive cycles, and applied at every call site.node_free(n); n->v,buf_destroy(&b); b.data[0], out-parameters, helpers that free globals and wrappers of wrappers are now checked without annotations. -
Shipped summaries for the C standard library (
mallocfamily,str*,mem*,stdio,strtol,getenv, …), including which results alias which arguments (strchr,strtol’s end pointer) so real headers need no annotations. -
annotation-mismatch(error): a definition’s body contradicts its ownWEAVEC_OWNED/WEAVEC_BORROWED/WEAVEC_MUTannotation (frees or writes through a borrowed parameter, moves aWEAVEC_MUTone, returns a borrow from aWEAVEC_OWNEDresult, …). Callers keep trusting the annotation. -
annotation-requiredis on by default at the external boundary: the first call to a function with no definition here, no annotations and no libc entry warns once per callee (system headers exempt).--strict-externsmakes it an error. With--report-unannotated, exported functions get one warning per unannotated pointer position carrying a fix-it that inserts the inferred annotation. -
core::DiagnosticcarriesFixItHints, bridged to Clang so-fdiagnostics-parseable-fixitsand editors can apply them. -
--dump-analysisprints asummary:line per function. -
scripts/corpus.py: runsweavecover real C projects (scripts/corpus/projects.json), tallies diagnostics per id and compares withscripts/corpus/baseline.json; a weeklyCorpusworkflow runs it. -
TranslationUnitAnalyzer(Analysis) andSummaryStoreas the public entry points for whole-TU analysis;FunctionAnalyzer::analyzenow takes the store. -
Raw pointers (RFC 0004): a pointer cast from an integer, declared
WEAVEC_RAW, loaded through another raw pointer, or handed out as raw by a callee is tracked as raw (core::RawTracker,AnalysisState::raw,ValueSource::raw()). Dereferencing it, releasing it, passing it to an owning or dereferencing parameter, or asserting a safe kind for it outside aWEAVEC_UNSAFEregion is the newunsafe-operationerror, with a note saying why the pointer is raw. Copying, comparing and converting it back to an integer are fine anywhere. -
WEAVEC_RAWannotation for pointer parameters, returns, variables, fields and function-pointer types (“no ownership guarantee”).weavec.his at header version 0.2. -
Laundering (RFC 0004): inside a
WEAVEC_UNSAFEregion, assigning a raw pointer to a place declaredWEAVEC_OWNED/WEAVEC_BORROWED/WEAVEC_MUTor returning it from a function whose return type is so annotated asserts that kind, so pointers can be brought back into the model at one explicit point (WEAVEC_UNSAFE { n = (struct node *)handle; } free(n);). -
Calls through function pointers (RFC 0004) are checked: the signature comes from ownership annotations on the function-pointer type (
typedef void (*dtor_t)(void *WEAVEC_OWNED);, fields, parameters), else from the join of the summaries of every function of that type whose address is taken in the translation unit (ops.drop = node_free;,qsort(..., cmp)). Callbacks are analysed before their callers. Pointers with neither get oneannotation-requiredwarning per function-pointer type. -
POSIX and common GNU/BSD functions in the shipped library table (about 490 entries, up from 85):
<unistd.h>,<fcntl.h>,<sys/stat.h>,<dirent.h>(opendir/closedir/readdir),<stdio.h>extensions (getline,getdelim,asprintf,popen/pclose,fmemopen,open_memstream),<stdlib.h>extensions (posix_memalign,reallocarray,realpath,mkstemp,qsort_r),<string.h>extensions (strtok_r,strsep,stpcpy,strlcpy,memmem, …),<time.h>,<sys/mman.h>(mmap/munmap),<pthread.h>,<sys/socket.h>,<netdb.h>(getaddrinfo/freeaddrinfo),<arpa/inet.h>,<dlfcn.h>,<regex.h>,<signal.h>,<sys/wait.h>,<poll.h>,<pwd.h>,<grp.h>,<iconv.h>,<glob.h>,<wchar.h>and more. -
--dump-analysisprints the raw component of the exit state (raw{r@3:13 integer-cast}),rawas a place kind and as a value source. -
core::FunctionSummary::inferredReturnKindreportsRaw;--report-unannotatedoffersWEAVEC_RAWfor a result inferred raw. -
SummaryStore::lookupIndirect,addAddressTaken,candidatesFor;analysis::collectFunctionTypeAnnotations;TranslationUnitAnalyzer:: collectAddressTaken. -
Whole-program analysis (RFC 0005): the unit of analysis is the program, not the translation unit. Every unit exports the summaries of its external-linkage definitions and address-taken functions; a
ProgramDatabase(Analysis) collects them andSummaryStoreconsults it between a unit’s own inference and the libc table, sonode_free(n); n->vis caught whennode_freeis defined in another file, a program’s ownstrdupis checked against its own body, and a callback installed in one file is joined into the signature of the function-pointer type it is called through in another. Units are analysed dependencies-first by strongly connected component (core::stronglyConnectedComponents,Core/Scc.h); mutually dependent units iterate to a fixpoint. -
weavec --whole-program [files] -- <flags>(or-p build/for every file of a compilation database) analyses the files as one program;--dump-analysisthen ends with the program database. -
weavec-cc, a drop-in C compiler: Clang’s driver plans the jobs, each-cc1runs in-process with WeaveC’s consumer multiplexed beside Clang’s code generation (one parse; a WeaveC error fails the compile). The compile step writes the unit’s exports, its cc1 command and the diagnostics it reported to<object>.weavec; the link step reads the sidecars of the objects on the link line, re-analyses the units whose results depend on other units, reports what needs two files (once), and refuses to link on an error. Objects without a sidecar are unknown code; a sidecar older than its object is ignored with a warning; sidecars of one-step builds (weavec-cc a.c b.c -o prog) are read and removed with the temporaries.-cc1asand other jobs are delegated toclang(WEAVEC_CLANG, the configuredWEAVEC_CLANG_EXECUTABLE, orPATH). -
Driver flags:
-fweavec/-fno-weavec,-fweavec-strict,-fweavec-report-unannotated,-fweavec-analyze-headers,-fweavec-dump-analysis,-fweavec-link/-fno-weavec-link. Warning control for both tools (frontend::DiagnosticControl):-Wno-weavec-<id>(warnings only),-Wweavec-<id>,-Wno-error=weavec-<id>,-Werror=weavec-<id>,-Wno-weavec,-Wno-error=weavec,-Werror=weavec. Disabling an error is refused with a message naming the-Wno-error=form. -
core::FunctionSummarytext format (Core/SummaryIO.h:printSummary/parseSummary,SummaryFormatVersion), Clang-free and round-trip tested; globals are spelled by name through callbacks.core::FunctionSummary::remapGlobals.core::diag::All,isKnown,isWarningByDefault. -
analysis::UnitExports,ExportedFunction,functionTypeKey,ProgramDatabase;SummaryStore::setDatabase,SummarySource::Program,isAddressTaken,unknownCalleeNames,unknownIndirectTypeKeys;TranslationUnitAnalyzer::setDatabase,discover(),exports();AnalysisOptions::deferBoundary. -
frontend::ProgramAnalysis(over an abstractProgramUnit;CompilationDatabaseUnit),frontend::Sidecar.h(UnitRecord,readSidecar/writeSidecar,weavec-summaries 1),frontend::Driver.h(DriverOptions,runDriver,runCc1),createWeaveCConsumer,FrontendOptions::{database, alreadyReported, boundaryOnce, silent, discoverOnly, onResult},UnitResult. -
WeaveCFrontendTestsunit-test binary;test/WholeProgram/andtest/Driver/rfc0005-*.clit suites. -
scripts/corpus.py:"whole_program": trueprojects are analysed as one program (--local-whole-programfor--local);cJSON-programandlinenoise-programadded to the corpus, with triage inscripts/corpus/README.md. -
Precision (RFC 0006). Loans end at the last use of their holder, not at the end of its scope: a backward liveness pass over the CFG expires the loans held by dead locals before each element, so
char *p = buf; use(p); buf[0] = 0;andint *a = &n->v; *a = 1; free(n);are clean whilefree(n); *a = 1;is stillcannot free 'n' while it is borrowed. Loans held through a pointer, by a global or by an address-taken local last until the holder is reassigned (BorrowState::expireHolders). -
Condition facts on CFG edges (RFC 0006):
p == q/p != qunite or separate the two pointers on the edge where the test holds, soif (l == sentinel) return; free(l); use(sentinel);is clean;!=separates only exact aliases.AliasRelationrecords per edge whether two places hold the same value or point into the same object (unite(a, b, exact),separateExact,isExact,edge); pointer arithmetic makes a copy interior (ValueOrigin::interior,ValueSource::interiorCopy, printedinterior <path>), and the libc table says which results are the argument itself (memcpy,strcpy,fgets,getcwd, …) and which point into it (strchr,strstr,strtok,bsearch,readdir, thestrto*end pointers, …). -
Element witnesses (RFC 0006):
a[*]is still one place, but a move record remembers which element was named (core::ElementWitness: whole, a constant, a variable, or unknown) and only an access with a matching witness is a use of it.free(a[i]); a[i][0] = 0;andfree(a[0]); free(a[0]);are reported;for (i) free(a[i]); free(a);,free(a[0]); use(a[1]);,free(a[i]); a[i] = NULL;andfree(a[i]); use(a[j]);are clean. Writing to, incrementing or taking the address of the index variable makes its witnesses unknown (MoveTracker::forgetWitness).*aon an array isa[0]. -
Outcome-conditional summaries (RFC 0006): a callee that frees or moves an argument only on the paths returning some class of value (
null/nonnull,zero/positive/negative) is summarised per class (core::Outcome,FunctionSummary::outcomes,addOutcome,consumesUnconditionally), and a caller’s test of the result (if (!q),q == NULL,rc != 0,rc < 0,rc == -1,(rc = f(p)) == 0, …) retracts the consumption on the edge where it did not happen.int rc = try_take(p); if (rc != 0) free(p);is clean andif (rc == 0) use(p);is ause-after-move; wrappers aroundrealloc(if (!q) return NULL; return q;) and around error-returning functions inherit the conditional behaviour, across units too.realloc’s null edge is now this mechanism (AnalysisState::pending,PendingOutcome,AnalysisState::consumed;reallocLike/isReallocare gone). Summary text format version 2:outcome <class>,outcome <class> <path> <flags>,interior <path>; sidecar format version 2 (weavec-summaries 2);--dump-analysisprints the classes (outcome zero{n: freed} outcome negative{}). -
A callee’s
writteneffect forgets every fact below the written place (RFC 0006), sofree(root->string); memcpy(root, &tmp, sizeof *root); use(root->string);is clean and the summary of such a body no longer claims the field is freed. Fortified__builtin___memcpy_chk-style variants of themem*/str*copy functions share their entries. -
--exclusive-borrows(weavec) /-fweavec-exclusive-borrows(weavec-cc),AnalysisOptions::exclusiveBorrows: enforce RFC 0001’s exclusivity between borrows (see Changed). -
zlib (15 units) and Lua (34 units) added to the corpus as whole programs. zlib is clean apart from two callback boundaries; every Lua report is triaged in
scripts/corpus/README.md. The corpus baseline now has nouse-after-freeoutside Lua, noconflicting-borrowoutside Lua and twodouble-freeoutside Lua (down from 15, 4 and 10). -
Resource lifecycle (RFC 0007). Every allocation,
WEAVEC_OWNEDparameter and fresh result puts a resource on the function’s books (core::ResourceTracker,AnalysisState::resources,ResourceRecord), and the checker reports what happens to it:leak(new, warning): the resource’s last holder goes out of reach (return, scope end, the statement after its last use, a value that is never read) without it having been released, moved, returned, stored into caller-visible memory, handed to unknown code or cast to an integer; also'<p>' is leaked: it is overwritten without being released,'<b>->p' is leaked when '<b>' is freed,result of '<f>' is leakedfor a discarded allocating call. Copies share one record and one report; a callee summarised from its body that records no effect on a pointer parameter is trusted not to retain it; the null edge of a test of the holder owns nothing (through__builtin_expectand(c) != 0too); a block ending in anoreturncall is not a death point, nor is the edge into one; a value stored below a pointer whose object nobody here owns or names (box = lua_touserdata(L, 1); box->buf = malloc(n)) escapes; a resource kept by a global orstaticlocal is not a leak.mismatched-release(new, error): every allocator in the shipped table belongs to the family of its releaser (malloc/strdup/realloc→free,fopen→fclose,opendir→closedir,getaddrinfo→freeaddrinfo,mmap→munmap,popen→pclose, …), and releasing a resource of one family with a function of another (free(fopen(...)),fclose((FILE *)malloc(8)),realloc(f, n)on aFILE) is reported, through wrappers defined in the program and across units. Inferred summaries carry the family (fresh(fclose),freed(free),moved(free);ValueSource::family,PlaceEffect::family,FunctionSummary::freshReturnFamily).WEAVEC_OWNEDon a struct field is now enforced:free(b)withb->pneither freed, moved, tested nor nulled is aleakofb->p, for objects that came from outside (a parameter, a global, a load). A field this function stored an owned value into is checked the same way without the annotation (b->data = malloc(8); free(b);), as are array elements (a[0] = strdup("x"); free(a);).- Per-outcome null facts (
FunctionSummary::nullOn,PendingOutcome:: nullOn): a constructor that reports failure through its result (int make(char **out) { *out = malloc(n); return *out != NULL; }, orif (*out == NULL) return -1;) is summarised with the classes on which*outis null or holds nothing the callee stored (the guard returns before the store,if (strm == NULL) return Z_STREAM_ERROR;), soif (!make(&s)) return; free(s);anderr = init(&s); if (err != 0) return err;are clean (RFC 0007, Per-outcome null stores). weavec.hheader version 0.2 → 0.3 (documentation ofWEAVEC_OWNEDon fields).-Wno-weavec-leakdisables the warning;-Werror=weavec-leakpromotes it.- Summary text format version 2 → 3:
freed(<family>),moved(<family>),fresh(<family>)andnull <class> <path>lines; sidecar format version 3 (weavec-summaries 3).--dump-analysisprints the exit state’s resources (owned{p@3:14 allocated free}) and the families in summaries; the program dump uses the same spellings.
-
Pointer validity (RFC 0008). Three new diagnostics, all errors by default (lower with
-Wno-error=weavec-<id>; they cannot be disabled):null-dereference: a pointer that is null or may be null on some path is dereferenced (dereference of '<p>', which may be null/which is null), or passed to a callee that dereferences its parameter without testing it ('<p>', which may be null, is passed to '<f>', which dereferences it). The newcore::NullTracker(AnalysisState::nulls) knows null constants, the results of every allocator and searching function in the shipped table (malloc,strchr,fopen,getenv,fgets,realpath, …), the results and out-parameter stores of every function in the program whose body can produce null, declaredWEAVEC_NULLABLEplaces, and pointers whose null test merged back. Every test idiom (if (!p),p == NULL,&&/||/?:,while ((q = f())),__builtin_expect) clears the fact on the non-null edge for the pointer and its copies; a callee’s outcome does too (if (!make(&p)) return -1; p[0]is clean whenmakereturns*out != NULL). Pointers the checker knows nothing about are trusted, a redundant retest of a pointer already known non-null does not make it maybe-null (cJSON’scan_access_at_index),(T *)0is a null constant likeNULL(zlib’sZ_NULLcasts), and a call into unchecked code forgets what it knew about the storage it was handed by address (a callback fillinglinenoiseCompletions lc = { 0, NULL }). A callee’s failing outcome class makes a place null only when the callee can have left null there: the RFC 0007null{...}entry for afreshstore that did not happen on that class (if (grow(b, n) == -1)) still drops the caller’s resource record but no longer says the buffer is null (linenoise’slinenoiseEditGrow, cJSON’sensure, zlib’sgz_init). Summaries carryrequires{s}(the parameters a function dereferences untested; everyr/wparameter of the shipped table except those documented to acceptNULL) andnotnull{*out}per outcome class;returns{...}listsnull.use-of-uninitialized: a pointer variable, or a pointer field of a record variable, declared without an initialiser is read, dereferenced, copied or released before it is assigned (use of '<p>' before it was initialized).core::MoveReason::Uninitialized; any assignment, a callee’s store, a mutable borrow for a call ormemsetinitialises it.invalid-release:free(or any releaser, or a consuming parameter) of a pointer to a stack or static object ('<p>' is released but points to '<x>', which is not a heap object), to a string literal, or into the middle of an allocation (p + 1,strchr(p, c):'<p>' is released but does not point to the start of its allocation;ResourceRecord:: interior). String literals are borrows of a static synthetic place.WEAVEC_NULLABLEandWEAVEC_NONNULL(weavec.hheader version 0.3 → 0.4): on parameters, return types, variables and fields; neither says anything about ownership. Both on one declaration isinvalid-annotation.- Replaced values: a callee that releases a caller-visible value and then
reinitialises the place (
free(b->data); b->data = NULL;,v->items = realloc(v->items, n)) now records the consume (freed,replaced/moved,replaced), so a caller’s copy of the old value (int *old = v->items; grow(v); old[0]) is ause-after-free/use-after-movewhile the place itself stays usable (RFC 0008, Replaced values). This closes the soundness hole RFC 0003’s exit-state rule left open and replaces RFC 0007’s Deliberately not caught entry for it. Only consumption on a path that returns counts:lua_close(L); exit(1);does not freeL->l_Gas far as callers are concerned. - Element consumes: a callee that frees an element (
free(history[len])) says so (freed,element), and the caller applies the consume with an unknown element witness (RFC 0006), so two such calls in a loop are not adouble-freeof*history. - Struct-by-value results: a function returning a record hands the caller
its pointer fields through the new
resultsummary root (stores{result.data = fresh(free)}), sostruct buf b = make();ownsb.dataand its leak is reported. - Summary text format version 3 → 4: the
replacedandelementflags,requires N,notnull <class> <path>and theresultroot; sidecar format version 4 (weavec-summaries 4).--dump-analysisprintsnulls{p@3:14 maybe-null, q nonnull}in the exit state,uninitializedmoves,interiorresources, and the new summary words.
-
Value-conditional behaviour (RFC 0009). No new diagnostic; three sources of false positives that every real code base hits are closed:
- Integer facts and guards: the new
core::ScalarTracker(AnalysisState::scalars) knows the class (zero,positive,negative) and, when it can, the exact value of an integer local, parameter or field, from constants assigned to it, from the edges of every comparison with an integer constant (n == 0,n > 0,!c,n != 3) and fromswitchcases. Every move, held resource and null record carries thePlaceGuardof the path that created it, and a later test that contradicts the guard drops the record on that edge:if (c) free(p); ... if (!c) use(p);, a secondswitchon the sameopcode, andchar *p = NULL; if (n > 0) p = malloc(n); if (n <= 0) return -1; ... free(p);are clean;int c = 0; if (c) free(p);does not take the branch at all. Writing the integer forgets the fact and weakens every guard that named it; a fact about memory behind a pointer never prunes an edge (aliasing), only the guards. - Argument-conditional summaries: a consume, store or return alternative
that happens only under a fact about the callee’s entry values is
summarised with a
whenguard (param 1 freed(free) when param 3 zero,return fresh(free) when param 3 positive|negative,store param 0 *.msg = copy param 2 when param 2 nonnull; at mostMaxGuardConjunctsconjuncts, never on a path the callee wrote). At the call the guard is translated to the arguments — a constant orNULLargument decides it, a variable is looked up in the caller’s facts — and the effect applies only when it is not refuted; what survives stays on the record in the caller.l_alloc(ud, p, 8, 0)freespand returns null,l_alloc(ud, p, 8, 16)returns a fresh block and keepsp;release(&b)withb.noalloc = 1frees nothing;gz_error(s, err, NULL)stores nothing.SummaryStorekeepsneverReturnsand guards across the per-type join and the program database. A value gone at the callee’s exit only under a guard (if (b == NULL) finish(L); else append(L, b);) is an unreplaced consume under that guard, not an unconditional one, so a caller whose argument refutes it is not told its stack was freed (Lua’sstr_writerunderdumpBlock). - Inferred
noreturn: a function whose every path ends inabort,exit,longjmp, a declarednoreturn/_Noreturncallee, an infinite loop or another such function is summarisednever-returns, in the same unit and across the program; a call to one ends the block (the liveness pass agrees, so nothing is leaked at the end of a block that is never left) and code after it is dead. A function that returns on some path returns; a call through a function pointer never returns only if every candidate never returns. - Summary text format version 4 → 5: the
never-returnsline and the optionalwhen <path> <fact> [and ...]guard oneffect,outcome,storeandreturnlines (fact ::= =<integer> | <class>[|<class>]*); sidecar format version 5 (weavec-summaries 5).--dump-analysisprintsscalars{n =0, m positive|negative}in the exit state,when[c =0]on guarded moves, resources and nulls (otherwise-nonnullon a null record that is non-null when its guard fails),never-returnsand thewhen[...]clauses in summaries. - Corpus (
scripts/corpus/README.md): thel_allocfamily of Lua reports (double-free,use-after-free,conflicting-borrow,invalid-releaseand the function-pointerleaks), theluaL_errornull-dereferences, cJSON’sp.bufferleak, zlib’sstate->msgleaks and thenoalloc/noreturncorrelations RFC 0006 and RFC 0008 listed under Accepted false positives are gone.
- Integer facts and guards: the new
-
Shared ownership (RFC 0010). The one-owner model gains shares, so reference-counted objects, the most common way C shares an object, are checked instead of misreported:
- Reference counts are inferred from the bodies that keep them: a
function that increments an integer field of its argument’s object
(
o->rc++,o->rc += 1,__atomic_fetch_add(&o->rc, 1, m),__sync_add_and_fetch(&o->rc, 1)) retains it (increment param 0 *.rc); one that frees the object when a decrement of the field reaches zero (if (--o->rc == 0) free(o),o->rc-- == 1, the atomic and__sync_forms, or a helper likedec_and_test(&o->rc)) releases a share (param 0 freed(free),share,count param 0 *.rc). The caller’sResourceRecordcounts shares:obj_ref(a)adds one, a copy of a holder with a surplus takes one (b = obj_ref(a)makesbits own share, an alias ofafor frees but not for releases),obj_unreftakes one and kills the name only when it was the last. A share released on a name this function never retained (a parameter) is a discipline: the name is dead, other shares live on. A plainfreestill kills every alias. - New messages on existing ids:
use of 'p' after its reference was released/reference released here,'p' is released twice/previously released here, and theleaknotereference taken herefor a share that is retained and dropped (void f(struct obj *o) { struct obj *p = o->next; obj_ref(p); }). A leak of a share is reported only through a known count: a field some function in the program releases through, or one annotatedWEAVEC_REFCOUNT; incrementing a length is not a leak. - Per-outcome stores: a callee that stores an argument only on some
outcome classes (
bag_putreturning-1when full) is summarisedstored zero param 0 *.items[]/stored negativeand the caller retracts the store on the classes it rules out, soif (bag_put(b, s) < 0) free(s);is clean and the failure edge that dropssis aleak. - Per-outcome integer facts: what a callee left in the caller’s integer
memory per class (
fact positive param 0 * =0fordec_and_test,fact negative param 0 *.filled =0) is known after the call on the edge the caller takes. - Annotations
WEAVEC_RETAINSandWEAVEC_RELEASES(pointer parameters: a library’sg_object_ref/g_object_unrefwith no body in view; a returning ref’s result is a copy of its argument),WEAVEC_REFCOUNT(an integer field is a count) andWEAVEC_OWNED_BY(f)(the release family of aWEAVEC_OWNEDresult or parameter, closing RFC 0007’s open item);weavec.h0.5.WEAVEC_OWNED_BYwithoutWEAVEC_OWNED, orWEAVEC_RETAINSwithWEAVEC_RELEASESon one declaration, isinvalid-annotation. - Stores out of sight: a callee that copies its argument into memory its
summary cannot name (
p->value = valuein a node it allocates and links into the caller’s table) saysparam 1 escaped, wrappers pass it on (throughtable_set_new(t, obj_ref(value))too: aparam iroot now resolves through a value that is a copy or null), and the caller neither reports the argument leaked nor loses the share it just took. Every container insert of an owned value (table_set(t, o)withofrommalloc) was a falseleakbefore. - Summary text format version 5 → 6: the
shareandescapedflags on effects and theincrement,decrement,count,storedandfactlines; sidecar format version 6 (weavec-summaries 6) withcount-field <key>lines.--dump-analysisprintssharesand the count field on resources,releasedmoves,escapedeffects,increments{},decrements{},counts{}and per-classstored{}/facts{}in summaries, andcount-fieldlines in the program database. - The whole-program fixpoint (RFC 0005) re-analyses a member of a cyclic
group only when the exports it imports changed since its last run.
Lua as one program (release build, same machine): 3 min 16 s before
this release, 2 min 16 s with shares and the fixpoint change, 3 min
22 s once
escapedeffects are applied at every call; the last figure is the quietest of several runs on a machine that was otherwise busy. - Corpus: jansson (reference-counted JSON, analysed as one program) joins
scripts/corpus/projects.json;{support}in a project’s arguments namesscripts/corpus/support/<project>, for headers a checkout’s build would generate.
- Reference counts are inferred from the bodies that keep them: a
function that increments an integer field of its argument’s object
(
-
Spatial safety (RFC 0011). A pointer is now an object and an offset into it, objects have a size, and accesses are checked against it:
- Derived pointers:
&p->f,&p[3],p + 4,p->payload(an array member decaying) and(char *)p - offsetof(struct outer, in)are the same object at a known offset (core::PointerOffset: zero, a constant number of elements, a field, or unknown), replacing the booleaninteriorflag on alias edges and resources.container_ofround trips (free(container_of(i, struct outer, in))freesi’s object,release(&o->in)on a wrapper that does so is adouble-freeofo),q != pseparates two derived pointers, and a field pointer kept acrossfree(p)is ause-after-freeon its next use (it was aconflicting-borrowat the free).invalid-releasenames the offset (points 4 elements past the start/points to field 'in'). Summaries record the offset a parameter was consumed at (param 0 freed(free) @-struct outer.in) and the offset of a returned copy (return copy param 0 @+struct list.next). - Extents:
malloc(n),calloc(n, sz),realloc(p, n)and every function in the program that returns a fresh allocation (return fresh extent=n), locals, arrays and array members of declared size, string literals andWEAVEC_SIZED_BY(n)parameters carry a size (core::Affine: a constant, orscale * place + constant), kept bycore::SpatialTrackerbeside the resource books. - Relations:
core::RelationTrackerrecordsi < n,i <= n,i >= n,i == nbetween integer places from condition edges (through one copyj = i), and a constant upper bound (i < 8); a write to either side forgets them. - New diagnostic
out-of-bounds(error):p[i],*(p + i),(p + i)->fand the buffer/length pairs of the shipped table (memcpy,memmove,memcmp,memset,memchr,strncpy,strnlen,fgets,snprintf,vsnprintf,strlcpy,strlcat,bzero,read,pread,recv,write,send,getcwd,readlink) are checked against the object’s extent: constant against constant, a symbolic index through the same counter (p[n]onmalloc(n)), a relation ('i' may equal 'n'underi <= n,'i' is at least 'n'underi >= n), or a constant bound ('i' may be 7 in an object of 4 bytesunderi < 8); the pointer’s own offset is added (p = buf + 4; p[4]); accesses before the start are reported too. Nothing is reported when the checker cannot relate the index to the extent. - Extent requirements in summaries: a callee that accesses more of a
parameter than its type promises (
b[7],for (i = 0; i < n; i++) b[i],memset(b, 0, n)) is summarisedrequires-extent{b: 8}/{b: n*4}and checked at every call ('put7' requires 8 bytes behind 'small', which has 4 bytes), through wrappers and across units; a need under a condition a summary cannot spell (min(n, 16),if (n > 4) b[4]) is not exported. WEAVEC_SIZED_BY(n)(pointer parameters: at leastnelements behind the pointer, bytes forvoid *): checked inside the body and at every call;invalid-annotationon a non-pointer or naming no integer parameter.weavec.h0.6.lifetime-too-shortis decided when the pointee dies rather than at the store: a store undone before then (ls->fs = fs.prev), or into a holder that is dead by then, is not reported. TheL->fs = &fs; ... L->fs = fs.previdiom that produced every Lualifetime-too-shortis clean.- The whole-program fixpoint (RFC 0005) widens a cyclic group after a fixed number of rounds (each member’s exports are joined with the previous round’s) so it converges on every input; the round cap is a safety net rather than the usual exit.
- Summary text format version 6 → 7:
@<offset>on effects and copies,extent=on fresh sources, andrequires-extentlines; sidecar format version 7.--dump-analysisprints offsets on alias edges (q~p@+4),spatial{}(extents and offsets) andrelations{}in states, andrequires-extent{}in summaries and in the program database dump. - Recall check:
test/recall/CWE-*/*.care Juliet-style cases (abadfunction with a// RECALL: <id> @<line>pin andgoodfunctions that must be clean) for CWE-121, 122, 124, 126, 127 (buffer overflows, underwrites and over-reads), 401 (leak), 415 (double free), 416 (use after free), 457 (uninitialised) and 476 (null dereference);scripts/recall.pyruns them, prints recall per CWE and fails on any missed pin or any report in agoodfunction. Wired intoctest(recall) and CI.
- Derived pointers:
-
Spatial safety II (RFC 0012): strings, sized fields, offset relations and assumptions.
- String facts: beside its extent an object carries the length of the
string it holds (a constant,
strlen(s)as a length place, or a variable equal to one) or the fact that it has no terminator, on the object and every exact alias of it. Sources: literals and initialisers;strcpy,stpcpy,strcat,sprintf,strdup,fgets,snprintf;strncpywith a source at least as long as the count,memsetwith a non-zero byte andmemcpyfrom an unterminated source over the whole object (no terminator); a NUL store; every other write forgets. out-of-boundsfor the string copies against the destination’s extent:'strcpy' accesses 6 bytes of 'buf', which has 4 bytes,'strcpy' accesses 'strlen(s)' + 1 bytes of 'd', which has 'strlen(s)' bytes(malloc(strlen(s)), also throughn = strlen(s)),'strcat' accesses 5 bytes of 'buf', which has 4 bytes(counting whatbufholds),'sprintf' accesses at least 5 bytes of 'buf', which has 4 bytes(the format’s minimum); and for terminator-seeking reads (strlen,strcpy’s andstrcat’s source,puts,printf("%s")) of an object known to have none:'strlen' reads past the end of 'name', which is not NUL-terminated, note'name' is left without a terminator here. A copy that overflowed leaves the string unknown, so it is reported once. The_FORTIFY_SOURCEforms of theprintffamily (__builtin___sprintf_chkand friends, which put a flag and a size before the format) have their own library-table rows, so the checks see them too; on macOS this also means an uncheckedmallocresult passed tosprintfis anull-dereferencethere as it already was on Linux (snprintf’s forms keep accepting a null destination).WEAVEC_SIZED_BY(g)on pointer fields (struct buf { char *WEAVEC_SIZED_BY(cap) data; size_t cap; }): every load of the field hascapelements of extent ('b->data[b->cap]' is out of bounds: 'b->cap' is the number of elements of 'b->data'), and a store of a smaller object is anannotation-mismatch('b->data' is declared WEAVEC_SIZED_BY(cap) but is given 4 bytes where 'b->cap' says 8, at the store or at the count’s write, whichever is second).invalid-annotationon a non-pointer field or one naming no integer sibling, once per unit.- Inferred sized fields: every store into a pointer field of a named
record is a witness (
v->items = malloc(n * sizeof *v->items); v->cap = n;) or a refutation (a store of an object no sibling counts; a write to a count with no store into the pointer); a pair witnessed with one count and scale and refuted nowhere in the program is in force for every load. Within a unit the readers of a newly confirmed field are analysed once more and only their new reports shown; across units the whole-program driver does the same for units that looked the field up before the program confirmed it, andweavec-cc’s link step re-analyses an object whose sidecar says it did. - Offset relations and lower bounds:
i <= n - 1isi < n,j = i + 1carries intoa[j]('a[i + 1]' may be out of bounds: 'i' may reach one below 'n', and 'a' has 'n' * 4 bytes), and a constant lower bound decides outright ('buf[i]' is out of bounds: 'i' is at least 8 in an object of 8 bytes). WEAVEC_ASSUME(expr):exprholds from the call on, as on the true edge ofif (expr); a contradicted assumption ends the path;weavec.assumeon any other function isinvalid-annotation.weavec.h0.7.- Sidecar format version 8:
sized-field <f> <g> <scale>,unsized-field <f> [<g>]andloads-field <f>lines (summary text format unchanged at 7).--dump-analysisprints string facts inspatial{}(s string=len(strlen(s))),i >= 8inrelations{}, and the program’s sized-field facts aftercount-field. - Recall: CWE-170 (improper null termination) joins the set; CWE-121,
122 and 126 gain the string shapes (
strcpyof a literal,strcat,sprintf,malloc(strlen(s)),strdupindexed past its end,strlenof an unterminated array) and CWE-122 the sized-field ones.
- String facts: beside its extent an object carries the length of the
string it holds (a constant,
-
Outcome classes keep their guards (RFC 0009, Guards).
if (nsize == 0) { free(ptr); return NULL; } return realloc(ptr, nsize);(Lua’sl_alloc) is summarisedoutcome null{ptr: freed when nsize =0} outcome nonnull{ptr: moved}instead of an unconditional free; a caller that tests the result and knows the size is non-zero still owns the block on the failure path, and the classes and their guards survive wrappers (if (nb == NULL && nsize > 0),if (nv == NULL) { /* keep */ } else t->hash = nv;). Two narrowings of one call’s outcome that meet at a join are one outcome again rather than being forgotten.
-
Pass optional access starts directly through bounds checking and requirement propagation, avoiding unwrap-and-rewrap conversions rejected by clang-tidy.
-
Cache constant operands during modular range multiplication so clang-tidy checks the same optional values it later reads, avoiding a Linux CI uninitialized-value false positive. Retain coverage for both operand orders. Use explicit empty defaults for optional product types and append expression descriptions without temporary string concatenations to satisfy header checks.
-
A null test of a pointer made every exact alias of it definitely null, including aliases the relation only holds on some path (two pointers into the same object at the same field on one loop iteration): Lua’s
if ((newci = luaD_precall(L, ra, n)) == NULL) updatetrap(ci);madecinull and every laterci->anull-dereference. A null claim now travels only to a copy whose own record still agrees with the tested place’s;NonNulltravels as before (RFC 0011, Deriving a pointer). -
A returned local that aliased both a caller’s place (
ci = L->ci) and the base of a derived pointer (Lat+base_ci) could be summarised by the derived name; the exact alias is preferred (return copy L->ci). -
Freeing an object reported a
conflicting-borrowfor a borrow of an object the freed one merely pointed at (*parents->buckets->last, a pair jansson’s intrusive list owns, whenhashtable_do_rehashfrees the bucket array). The check now looks only at loans on the released storage (the object and its fields, without crossing another dereference); what the object’s pointers own is released by a consume of its own, with its own check (RFC 0011, Deriving a pointer). -
f = fs->f; f->upvalues = grow(...); return &f->upvalues[n](Lua’sallocupvalue) was summarised as returning a fresh allocation, so every caller leaked the pointer it got back ('up' is leaked); it is a copy of the caller’s ownfs->f->upvalues(RFC 0011, Deriving a pointer). -
A
voidcallee that frees what an argument points into only under a guard (moveresults:res: freed when[fwanted positive|negative]) had every copy of that argument read as a fresh block handed back;consumesUnconditionallynow honours the effect’s own guard when there are no outcome classes (RFC 0009, Outcome classes). -
An integer constant was read as its two’s-complement bits rather than its value, and every comparison as signed:
ULONG_MAXwas-1, so forindex = 0the edge on whichif (index > ULONG_MAX)fails was infeasible and everything after it unchecked (anull-dereferencein cJSON’screate_patcheswent unreported). A constant is now its mathematical value in its own type (unsigned x = -1isUINT_MAX;SIZE_MAXis no constant the checker holds and decides nothing), an unsigned comparison is decided in unsigned order (a negative signed operand converted up ranks above every non-negative value) and acaselabel is converted to the scrutinee’s type (RFC 0009, Assumptions). -
A callee that released a value the caller had already released and left a new one in its place (
replaced, RFC 0008) was reported once and then again at every later call, because the stale record survived when no store named the place in the caller’s terms (the callee reached it through an alias of its own). The place now holds the new value after the one report. -
A callee whose only effect below a pointer argument was a consume (
free(L->stack)) was replayed into its caller’s summary as having overwritten the whole object (*L: written), which made the caller’s callers forget everything belowL, including thatL->stackwas gone. Only a summary with no effect at all below the pointee is that coarse (RFC 0006,writtenforgets what lies below). -
free(static_array)andfree(local_array)crashed the checker (getPointeeTypeon an array declaration incheckContainerFree); they are nowinvalid-releasereports (RFC 0008). -
The whole-program fixpoint (RFC 0005) rebuilt its database by renumbering every member’s summaries each time one member’s exports changed, which on Lua (34 units in one cycle) was most of the run once RFC 0008 made the summaries larger. Members are now kept numbered by the database’s own table (
ProgramDatabase::renumbered,GlobalNames::extendTo) and a rebuild copies them. -
BorrowStatekept one loan per site of a borrow, so a callee’s store applied at a hundred call sites was a hundred loans copied at every block visit and merged at every join (1,173 per state in Lua’sluaV_execute). It keeps one per borrow at the earliest site, which is the one a conflict cited anyway (BorrowState::sameBorrow). -
Applying a callee’s summary consumed shallower paths before deeper ones, so
static void both(struct box *b) { free(b->p); free(b); }followed by a use of a caller’s copy ofb->pwas not reported: freeingbfirst dropped the facts under it beforeb->p’s consumption reached the alias. Consumed paths are now applied deepest first; memory below an object the same call frees is consumed on its own only when the caller knows the place, and reported once (at the object) when the object is already gone (RFC 0007, Applying a summary: deepest paths first). -
putenvkeeps its string andsetvbufkeeps its buffer in the shipped table (RFC 0007, Assumptions). -
a->fon an array (printbuffer buffer[1]; buffer->buffer = ...) is the placea[0].f, as*aalready wasa[0]; it used to be the element summarya[*].f, whose null store could not clear a record. -
Condition facts on the right operand of
||/&&: the block that evaluates(p = malloc(n)) == NULLinif (fd == -1 || (p = malloc(n)) == NULL) return NULL;branches on that operand, but Clang hands back the whole condition; the null edge did not clearp’s record and the return was a falseleak. -
Freeing memory reached below a borrowed object is not a
conflicting-borrow:z_streamp strm = &state->strm; inflateInit2(&state->strm, ...), whose callee freesstate->strm.state->window, leftstrmpointing at storage nothing released. Loans on ancestors of the consumed place no longer count for the consume query (RFC 0006, Conflict rules, amended). -
A
leakat the end of an address-taken local’s scope is reported on the statement the scope ends after (thereturn), not on the declaration. -
A block ending in a
noreturncall no longer flows into the function’s exit state, so what such a path frees (Lua’sos_exitclosing the state behindexit) does not become afreedeffect of the summary and adouble-freeat every caller; the summary describes the state after a return (RFC 0007, Interaction with existing RFCs). -
On the null edge of a pointer test every fact below the pointer is dropped (there is nothing there), so records, moves and aliases of the pointer’s fields no longer report on a path where the pointer is null.
-
A whole write to a field also clears the move records of the same cell under the pointer’s aliases (
free(L->stack); L->stack = freshwithL->twups ~ LleftL->twups->stackfreed for good), which on Lua made every secondluaD_reallocstackadouble-free(RFC 0002, Alias relation, implementation notes). -
An
&&/||under!,__builtin_expector!= 0is computed as a value before the branch, so only the whole value is known on an edge: a true&&or a false||now holds each operand (Lua’sl_unlikely(newblock == NULL && nsize > 0)makesnewblocknull, and overwriting it is noleak), and the other edges say nothing, where the right operand alone used to be taken (if (__builtin_expect(n > 0 && p != NULL, 1)) { free(p); return 0; } return -1;was wrongly clean). -
Two paths of one callee summary that name the same cell through the caller’s aliases (
g->allgcandg->twups->l_G->allgcwithg->twups ~ L, Lua’sluaC_freeallobjects) are one release, not adouble-free(RFC 0007, Applying a summary: deepest paths first). -
A value stored below a local that borrows caller memory or a global (
tb = &G(L)->strt; tb->hash = newvect, Lua’sluaS_resize) escapes: the store landed in an object that outlives the function under a name the summary cannot report, and the last local name’s death was a falseleak(RFC 0007, Escape).
Changed
Section titled “Changed”-
The corpus baseline (
scripts/corpus/baseline.json) is refreshed for RFC 0011:conflicting-borrow16 → 2,lifetime-too-short18 → 4,null-dereference189 → 171,leak20 → 17,invalid-release6 → 5,double-free81 → 82,use-after-free439 → 663 (the last two Lua’sluaV_executefamily, larger now thatci->func.pandci->top.pare named as pointers into the stack),out-of-bounds0; whole corpus 105 s (Lua from 3 min 22 s to 1 min 46 s). The per-project triage is inscripts/corpus/README.md. -
Dereferencing an allocator’s result without testing it (
p = malloc(n); p->x = 1;) is now anull-dereferenceerror (RFC 0008). The existing unit-test and lit snippets that did this gained anif (!p) return;; the inferred summaries of functions that return or store an untested allocation gainednullamong their returns / stores, and functions that dereference a parameter gainedrequires{...}. -
The RFC 0007 leak check found real leaks in the existing unit-test snippets (
reallocsuccess paths, anOWNEDparameter that was only looked at, aswitchwithoutdefault, a loop whosefreeis behindbreak); their expectations now include theleak. -
A callee’s summary store into a place that holds a resource this function made owned escapes the old value instead of reporting an overwrite: RFC 0003 shows the caller only the store for the far more common
free(b->data); b->data = NULL;(RFC 0007, Deliberately not caught). -
A call with a mutably borrowed argument copies the callee’s written paths below the argument into the caller’s summary (
L->nci: written,L->ci->top.p: written, …) instead of marking the pointee itselfwritten, which told every caller the whole object had been overwritten and made it forget everything it knew below the argument (Lua’sclose_statelost the freesfreestackreported to it). Summaries on Lua-sized programs are an order of magnitude longer as a result; the paths are resolved incrementally, cached per call while no new place is interned, and copied once per call and pointee, but the Lua program still takes about twice as long as before (RFC 0006, What a callee wrote, its caller wrote; RFC 0007, Performance). The three new Luaconflicting-borrowreports are facts this used to erase (close_statefreesL->l_GwhileL, an interior pointer into it, is live), of the interior-pointer kind RFC 0001 already accepts. -
A callee that consumes a path below an argument or a global and returns a
copyof it (t->array = resizearray(L, t, ...)) makes the result the same resource, now owned by the result, as RFC 0003 already did for consumed parameters (RFC 0006, Interaction with existing RFCs). Only when the path is moved or consumed on every class: a copy of a pathfreedon some classes only (zlib’sgz_lookcopyingstate->out, whichgz_fetchfrees on its error class) is not tracked, otherwise the copy’s later overwrite was a falseleak(RFC 0007, Applying a summary: deepest paths first). -
(res = feed()) == sentineland(p = f()) != qare pointer-equality condition facts aboutres/p(RFC 0006), so the “loop until the reader returns something other than the sentinel” idiom is clean. -
Analysis time on large functions and programs. A dead local’s alias edges are dropped with its loans (RFC 0006, Performance);
AnalysisState::joinreports whether it changed instead of the engine copying and comparing states;PlaceTable::descendantswalks the subtree instead of the table;BorrowStatekeeps its loans sorted; loan expiry runs only where a variable dies; the whole-program database is rebuilt only after a unit’s exports changed. Lua’slvm.cwent from 56 s to 3 s and the Lua program from 23 minutes to under 6. No diagnostic changed. -
conflicting-borrowno longer rejects two live borrows of one object or a write to a borrowed object by default (RFC 0006, Conflict rules):int *a = &x; int *b = &x;,char *p = buf; snprintf(buf, ...); use(p);andint *pa = &s->a; s->a = 2; use(pa);are accepted. Freeing or moving a borrowed object is still reported.--exclusive-borrowsrestores the RFC 0001 rule with the messages unchanged. -
free(a[0]); use(a[1]);and the loop-free idiom are no longer reported (RFC 0006, Element witnesses);free(a[i]); use(a[i]);still is. -
if (free_if(p, c)) return; p[0] = 1;is clean whenfree_iffrees only on the paths returning non-zero (RFC 0006); untested, the may-free is still reported. -
ValueSource::copyfromstrchr-like table entries is nowinteriorCopy;Storevalues forstrto*end pointers andstrtok_r’s save pointer likewise. -
SummaryFormatVersion1 → 2 andSidecarFormatVersion1 → 2: version-1 sidecars are rejected by their header and the objects re-analysed. -
MoveTracker::markMoved/movedAt/reinitializetake anElementWitness;AliasRelation::unitetakes exactness and witnesses;FunctionSummary::reallocLikeandCallEffects::isReallocare removed;AnalysisState::reallocsis replaced bypending. -
A whole-struct assignment or initialisation (
b = a,struct buf b = a;,*p = s) copies the facts of every pointer-typed field (aliases, loans, moves, raw records, kinds) instead of forgetting them (RFC 0005), sob = a; free(a.data); b.data[0]is ause-after-free. Fields with no place under the source are still forgotten. -
annotation-requiredfires for a callee with no definition anywhere in the program (previously: in the translation unit), once per callee per program; inweavec-ccit is deferred from the compile step to the link step. The second note now endsor define it in this program; the indirect form saysin this programinstead ofin this translation unit. -
--dump-analysisin whole-program mode prefixes each unit’s dump withunit '<source>':. -
tools/weavecaccepts zero source files with-p(every file of the database is analysed). -
SccFindermoved fromTranslationUnitAnalysis.cpptocore::stronglyConnectedComponents(Core/Scc.h). -
cmake/WeaveCLLVM.cmakerecordsWEAVEC_CLANG_EXECUTABLE;frontend::getClangExecutable(). -
WEAVEC_UNSAFEblocks and function bodies are analysed instead of skipped (RFC 0004): diagnostics inside the region are suppressed, raw operations are permitted, but ownership effects flow out of it, soWEAVEC_UNSAFE { free(p); } p[0]is now ause-after-freeatp[0]and aWEAVEC_UNSAFEfunction’s callers see its inferred effects. AWEAVEC_UNSAFEdeclaration without a body is unchanged (empty summary, no boundary warning). -
Pointer arithmetic (
p + 1,p++,&a[i]) and casts between pointer types preserve the identity of the pointed-to object instead of producing an untracked value (RFC 0004), soq = p + 1; free(p); q[0]is ause-after-freeandbind(fd, (struct sockaddr *)&addr, len)borrowsaddr. Only integer-to-pointer casts lose identity, and they yield a raw pointer. -
--strict-externs(RFC 0004): a call the checker cannot resolve is now anunsafe-operationerror at every call site outside aWEAVEC_UNSAFEregion (system headers included) and its pointer result is raw, instead of anannotation-requirederror once per callee. Inside an unsafe region it is silent. -
annotation-required’s notes mentionWEAVEC_RAW; a new form covers calls through function pointers with no signature. -
analysis::AnnotationSet::safeKind()returnsstd::optional<core::OwnershipKind>;AnnotationSetgainedraw,ownership()andmerge();analysis::ValueOrigingainedRawandrawReason. -
double-freeanduse-after-freeare now also reported for the second iteration of a loop that frees a pointer declared outside it. -
AnalysisOptionsgaineddumpStream;core::Loangainedholderandcore::MoveRecordgainedvia. -
Copying a pointer that holds a loan into a longer-lived pointer (
g = pwherep = &local) is nowlifetime-too-short, likeg = &localalready was. -
Calls to unannotated functions now borrow their pointer arguments for the duration of the call (previously: no effect), so
set(&x)whilexis borrowed is aconflicting-borrow. -
--report-unannotatedno longer reportsstaticfunctions ormain, and its message names the inferred annotation. -
analysis::CallEffectsis now backed by acore::FunctionSummary(releasesArgsreplaced byfrees(i);classifyCalltakes aSummaryStore).
Removed
Section titled “Removed”LocalOwnershipChecker(superseded byFunctionDataflow).