Repository navigation
External Models
The analyzer reads model files that describe API behavior, so a library's contracts do not have to be hardcoded. Three models exist, each with its own flag:
| Model | Flag | Feeds |
|---|---|---|
| Resource lifetime | --resource-model=<path> |
ResourceLifetime.* |
| Stack escape | --escape-model=<path> |
StackPointerEscape |
| Buffer write | --buffer-model=<path> |
MemcpyWithStackDest, copy/string overflow checks |
Bundled models live under models/, one generic.txt per family.
- Lines starting with
#are comments; empty lines are ignored. - Argument indices are 0-based.
-
<function-pattern>supports exact names and the globs*and?. - Matching is tried against the mangled name, the demangled name and the demangled base name.
Character classes are not supported.
[...]is rejected with an explicit parse error in the escape and buffer models — only*and?are implemented. Some bundled model files still carry a header comment mentioning[ ... ]; the parser is authoritative.
acquire_out <pattern> <arg> <kind> [if_ret...]
acquire_ret <pattern> <kind> [if_ret...]
release_arg <pattern> <arg> <kind> [if_ret...]
noeffect <pattern>
| Directive | Meaning |
|---|---|
acquire_out |
The function writes a new handle to the out-parameter at <arg>
|
acquire_ret |
The function returns a new handle |
release_arg |
The function releases the resource passed at <arg>
|
noeffect |
The callee neither releases nor retains any pointer it receives — string and IO libc functions |
<kind> is a user-defined string (HeapAlloc, VkBuffer, …) used to pair acquires with
releases. It must match between the acquire_* and release_arg directives.
An optional trailing qualifier makes the effect conditional on the call's return value:
if_ret==0, if_ret!=0, if_ret>=0, if_ret<0, if_ret==null, if_ret!=null
Without it the effect is unconditional. This is what lets a model describe an API that only hands over ownership when it succeeded:
acquire_out vkCreateBuffer 3 VkBuffer if_ret==0
noeffect matters as much as the acquires: without it, a handle passed to strlen or fprintf
looks like it might have been retained, and the analysis cannot conclude.
# C heap allocation
acquire_ret malloc HeapAlloc
release_arg free 0 HeapAlloc
# C++ heap allocation (demangled names contain spaces, use globs)
acquire_ret operator*new* CppHeap
release_arg operator*delete* 0 CppHeap
# Custom API, ownership only on success
acquire_out acquire_handle 0 GenericHandle if_ret==0
release_arg release_handle 0 GenericHandle
# Vulkan
acquire_out vkCreateBuffer 3 VkBuffer
release_arg vkDestroyBuffer 1 VkBuffer
# Callees that keep nothing
noeffect strlen
noeffect fprintf
models/resource-lifetime/generic.txt covers C malloc/free, C++ new/delete, 20+ Vulkan
resource types, and the noeffect entries for common libc functions.
With a resource model and more than one input file, the analyzer builds cross-TU summaries to propagate ownership across translation units.
| Option | Effect |
|---|---|
--resource-cross-tu |
Enable (default: on) |
--no-resource-cross-tu |
Local-only analysis |
--resource-summary-cache-dir=<path> |
Filesystem cache (default .cache/resource-lifetime) |
--resource-summary-cache-memory-only |
In-process cache only, no filesystem writes |
When ownership still cannot be decided, the analysis says so — ResourceLifetime.AnalysisIncomplete
(Info) or ResourceLifetime.IncompleteInterproc (Warning) — rather than reporting nothing. See
Diagnostic Rule Catalog.
Declares which arguments are consumed synchronously and not captured, which suppresses false
StackPointerEscape reports for APIs that use a pointer during the call only.
noescape_arg <pattern> <arg-index>
noescape_arg is the only directive; anything else is a parse error naming the line.
# Vulkan descriptor updates consume pointer arrays immediately
noescape_arg vkUpdateDescriptorSets 2
noescape_arg vkUpdateDescriptorSets 4
-
LLVM attributes at the call site:
nocapture,byval,byref - Interprocedural summary, for functions whose definition is analysed
-
External escape model: the
noescape_argrules - Opaque external call: no strong escape diagnostic is emitted
The model only has to describe what the first two steps cannot see.
Bundled: models/stack-escape/generic.txt.
Declares which calls write into a destination buffer, and whether the write is bounded by a size argument. This is what the copy and string overflow checks read.
bounded_write <pattern> <dst-arg-index> <size-arg-index>
unbounded_write <pattern> <dst-arg-index>
| Directive | Meaning |
|---|---|
bounded_write |
The call writes into <dst-arg-index>, limited by <size-arg-index>
|
unbounded_write |
The call writes into <dst-arg-index> with no size argument |
The distinction drives the CWE: a write with an explicit length maps to CWE-121, one without
to CWE-120 — see Diagnostic Rule Catalog.
# Bounded byte writes/copies
bounded_write memcpy 0 2
bounded_write memmove 0 2
bounded_write memset 0 2
bounded_write strncpy 0 2
bounded_write strncat 0 2
bounded_write stpncpy 0 2
# Fortified / wrapped forms frequently seen in optimized builds
bounded_write *__memcpy_chk* 0 2
bounded_write *__strncpy_chk* 0 2
# Unbounded writes (no explicit size argument)
unbounded_write strcpy 0
The fortified *_chk patterns matter in practice: an optimized build replaces memcpy with
__memcpy_chk, and without the pattern the model would silently stop matching.
Bundled: models/buffer-overflow/generic.txt.
- Start from the bundled models and extend them.
- Use globs for C++ overloads:
operator*new*. - Keep resource kinds consistent between
acquire_*andrelease_arg. - Add
noeffectentries for callees that keep nothing — it is what turns "cannot conclude" into a verdict. - Use
if_retwhen an API only transfers ownership on success. - Do not use
[...]: it is rejected.
A test source names its models in a comment:
// resource-model: models/resource-lifetime/generic.txt
// escape-model: models/stack-escape/generic.txt
// buffer-model: models/buffer-overflow/generic.txtSee Test Infrastructure.