Dynamic storage¶
CppVerify has a deliberately bounded, sound model for local scalar
new/delete. It tracks allocation ownership, lifetime, alignment,
extent, and initialization in SSA-versioned metadata rather than treating
valid_ptr as a timeless predicate.
Supported forms¶
The current surface accepts ordinary throwing scalar new for complete,
non-volatile integer or enumeration objects in executable contracted
functions. The result must directly initialize a local raw pointer:
int roundtrip(int value)
post(result == value)
{
int *p = new int(value);
int observed = *p;
delete p;
return observed;
}
Direct initialization and value initialization create initialized storage. Default initialization does not:
int write_before_read(int value)
post(result == value)
{
int *p = new int; // the pointee is uninitialized
*p = value; // this store marks it initialized
int observed = *p;
delete p;
return observed;
}
int zero_initialized()
post(result == 0)
{
int *p = new int(); // value-initialized to zero
int observed = *p;
delete p;
return observed;
}
Multiple simultaneously live scalar allocations are proved disjoint. A later
allocation may reuse storage after delete without contradicting facts about
the earlier lifetime.
Safety obligations¶
The verifier maintains these logical maps alongside the value heap:
allocation[address]The unique lifetime identity that owns each target byte.
base[identity]The numeric base address chosen for that lifetime.
live[identity]Whether that lifetime is active.
issued[identity]Whether that lifetime token has ever been allocated, preventing reuse.
initialized[address]Whether reading the scalar cell is defined.
size[identity]andalignment[identity]Target-layout metadata derived from Clang.
On a successful normal new path, the chosen base is non-null, satisfies the
target alignment, and does not overlap any currently live allocation byte. The
allocation map and metadata receive fresh SSA versions. A first-class
provenance companion travels with each supported local pointer value. A load or
store requires the byte owner to match that companion and its identity to remain
live; a load additionally requires initialization. delete requires the
matching identity and its exact base, then updates a new liveness version to
false.
An allocator may later choose the same numeric address. That replacement gets
a different identity, so the old pointer does not become valid again even when
old_pointer == replacement numerically.
Matching-type local copies, assignments, conditional selections, and
nullptr assignments update the address and provenance companions together:
int alias_write(int value) post(result == value) {
int *first = new int(0);
int *owner = new int;
int *alias = first;
alias = owner;
*alias = value;
int observed = *owner;
delete alias;
delete first;
return observed;
}
Deleting through either name ends the shared lifetime. Every alias then becomes
dangling, so dereferencing owner after delete alias is rejected.
Contracted scalar interfaces¶
A live initialized allocation pointer may cross a checked modular interface
when the callee is a verified in-translation-unit executable function with a
direct, matching-typed pointer parameter. The callee may compare or directly
dereference that parameter, and may write it under modifies(*p):
void write_value(int *target, int value)
pre(target != nullptr)
modifies(*target)
post(*target == value)
{
*target = value;
}
int modular_write(int value) post(result == value) {
int *p = new int(0);
write_value(p, value);
int observed = *p;
delete p;
return observed;
}
The caller substitutes the allocation’s lifetime identity into the callee precondition, so stale and uninitialized arguments fail at the call site. Owned dynamic storage satisfies the caller-side frame containment check only when the current provenance equals an identity actually allocated by that caller.
The checked interface may forward the direct pointer through an acyclic chain of verified scalar callees. Scalar values loaded from it may also pass through verified executable or pure spec helpers. Every reached body is scanned against the same direct-scalar rule; an offset, subscript, pointer copy, recursive scan cycle, or pointer-result intermediate makes the outer dynamic call fail closed.
A verified callee may return the direct dynamic formal, nullptr, or a
conditional selection of direct dynamic formals. VCallStmt gives the
pointer result a separate SSA provenance output. Generated result
validity/initialization clauses bind that output to the current byte owner, and
a contract such as post(result == source) preserves the source identity:
int *identity(int *source)
pre(source != nullptr)
post(result == source)
post(*result == old(*source))
{
return source;
}
int returned_alias(int value) post(result == value) {
int *owner = new int(value);
int *alias = identity(owner);
int observed = *alias;
delete alias;
return observed;
}
The pointee postcondition above is needed because an ordinary pointer-taking modular call conservatively forgets the value heap unless its contract restores the value. Provenance alone proves which live object the pointer denotes; it does not invent a value-preservation promise.
This remains deliberately a scalar interface. The callee may not offset,
subscript, copy, rebind, or deallocate the dynamic formal. Pointer-returning
calls nested inside the callee, proof functions, body-less external contracts,
ghost use, allocation inside the callee, type erasure, and a returned local
pointer copy are rejected. A weak pointer-result contract may be read as an
arbitrary valid pointer, but cannot gain caller-owned modifies or
delete authority without proving equality to an owned result.
Ordinary modular heap framing still applies: a read helper that must return the
entry value should state post(result == old(*p)).
Fresh-owned pointer results¶
A separate body-derived effect supports factories that create and return one owned scalar allocation:
int *make_value(int value)
post(result != nullptr)
post(*result == value)
{
int *owner = new int(value);
return owner;
}
int use_factory(int value)
post(result == value)
{
int *p = make_value(value);
int observed = *p;
delete p;
return observed;
}
There is deliberately no contract keyword that claims freshness. CppVerify infers the effect only from a body-present executable function when a conservative path analysis establishes all of the following:
the function has no pointer parameters or explicit
modifiesclause;every return path returns
nullptror the exact live base of one fresh scalar allocation of the declared pointee type;the allocation is fully initialized before return and has not been freed;
no other allocation remains live and the pointer has no secondary escape;
the result was not produced by pointer arithmetic, type erasure, an external call, or a recursive ownership cycle.
A direct store may initialize default-created storage. Matching local aliases, conditional/null returns, and direct or local-alias forwarding of another inferred factory are supported. The fixed-point inference is intentionally acyclic, so nested forwarding composes without trusting arbitrary call contracts.
At the call site, the verifier materializes a never-reused lifetime identity,
target size and alignment, byte ownership, liveness, initialization, and an
arbitrary typed scalar value. The new range must be disjoint from every live
represented object, live pointer value, stored pointer cell, and declared
valid extent. The ordinary postcondition then constrains that arbitrary
value; for example, post(*result == value) supplies the functional fact
used above. A nullable factory guards all allocation metadata on
result != nullptr.
The caller owns the materialized identity and may copy, forward, mutate, or delete it through the supported scalar-pointer operations. Deletion through one alias invalidates all aliases, so double delete and stale dereference remain proof failures.
Consequently, all of these are rejected:
int use_after_delete() post(true) {
int *p = new int(1);
delete p;
return *p; // use after lifetime
}
int double_delete() post(true) {
int *p = new int(1);
delete p;
delete p; // no live allocation remains
return 0;
}
int uninitialized_read() post(true) {
int *p = new int;
return *p; // no preceding store
}
The ordinary throwing allocation failure path exits exceptionally, so it does not reach a normal postcondition. Exception handling itself is not in the verified subset.
Current boundary¶
Matching-pointee local pointer values may be copied, reassigned, selected by a
conditional, or set directly to nullptr. Their provenance follows the value
through branch SSA merges. They may be dereferenced, stored through when the C++
type permits, compared for equality, converted to bool, deleted, passed
through the checked verified scalar interfaces above, or returned by the
inferred fresh-owned factory boundary.
Type-erasing or indirect copies, ownership-taking pointer parameters,
arithmetic-derived ownership, uninitialized/freed/multiple/escaped results,
recursive ownership cycles, proof/external ownership sources, general
subscripting, aggregate storage, and pointer reassignment inside loops remain
unsupported. The bounded same-object difference described in Pointers
may use a direct dynamic base or its inline +0/+1 position because the
lifetime identity proves the common origin. Functions that allocate cannot yet
also accept pointer parameters, and allocation/deallocation inside loop bodies
is rejected.
Also unsupported are new[]/delete[], nothrow or placement allocation,
non-scalar objects and destructors, and user-declared/general allocation-effect
contracts.
General same-array subtraction is available for abstract valid(p, n)
interfaces, but scalar dynamic allocations retain only their base and one-past
positions. Unsupported cases fail closed instead of falling back to the
abstract parameter-pointer model.
Pointer parameters remain abstract interface objects. Their implicit validity
and initialization assumptions are not derived from a concrete caller
allocation, and valid(p, n) remains the explicit buffer-extent marker. See
Pointers and Current limitations and C++ readiness.