Chapter 16 — When verification fails¶
A failed proof means the verifier found inputs or paths that break your stated properties—treat that as actionable feedback on the code, the contracts, or both.
Understanding the report¶
Diagnostics name the source-anchored obligation that failed and often include a
counterexample: typed source-name values that violate the claim, followed by
guarded branch, call, loop, heap/provenance, lifetime, and return events. Use
those values and events to see which assumption or branch is wrong. Values that
Z3 does not determine remain <unknown>; CppVerify does not complete the
model by inventing them.
For editor, CI, and agent integration, run:
cpp-verify --diagnostics-format=json file.cpp
Each verification result is one cppverify.diagnostic/1 JSON object with a
stable reason code, backend and optional BMC bound, inclusive source range,
obligation ID/kind, typed model values, and trace. An unknown model value is
JSON null. An undetermined path guard is "active": null rather than an
arbitrary branch choice. Command-line and frontend parse errors may still be
text, and IR dumps intentionally make the stream mixed. Malformed display bytes
are replaced with the Unicode replacement character, so JSON output remains
valid UTF-8.
An undecided query is different from a counterexample. Unresolved with
reason solver.timeout, solver.resource-limit, or solver.unknown
means Z3 timed out, exhausted an explicit resource/query budget, or entered a
fragment it could not decide, often because the VC combines quantifiers with
heap arrays. CppVerify may retry smaller ordered obligations, but if those also
remain undecided it reports the function Unresolved. It never treats solver
uncertainty as success.
A recursive spec applied beyond its unfolding fuel, or a spec you cppverify::hide, is
an opaque value that a solver model may choose freely, so a model is a
counterexample only if it still fails under the spec’s real definition.
CppVerify checks every model from every solver against the definitions. When
the check refutes it, CppVerify gives the solver the definition at the
disputed arguments and solves again, which can also prove the obligation. If
that does not settle the query, the result is not verified with reason
spec.fuel rather than a counterexample. Before reporting it, the verifier
tries to prove the goal by induction on its own (section “Automatic
induction” of Chapter 13 — Spec and proof functions); the message names the
inductions it tried, as induction following fibo and induction on n did not
prove it, and for a proof function shows a body to start a proof by
induction from. When the solver’s counterexamples need the spec at ever
larger arguments, the message says no finite unfolding settles it: the goal
needs an induction the verifier did not find, often one over a stronger
statement, and the fix is a lemma (that section works through one).
Otherwise raise cppverify::reveal_with_fuel, bound the argument, or state a lemma.
When the solver proposes a counterexample whose spec values its check cannot
compute, such as fibo(1000000000), the verifier tries to confirm it by a
proof at that input, from facts that are proved: the postconditions of specs
and the contracts of cppverify::proof functions that are established, each lemma
with every assumption its own proof made (section “Counterexamples too large
to compute” of Chapter 13 — Spec and proof functions). A confirmed
counterexample is a failure that says so (confirmed by a proof at this
input that uses the contract of ...). Otherwise the message shows the
proposed input and both possibilities: the claim is false there, or it is
true and needs a proof by induction. The verifier cannot tell these apart,
since neither the solver’s proposal nor the missing value decides it; prove
the claim if you believe it, or state the lemma that refutes it if you do
not. JSON gives the proposed input as unchecked_counterexample. For a
hidden spec the reason is
spec.hidden: the definition would settle the query, but cppverify::hide keeps it
out of proofs, so reveal it or state a lemma. A model that cannot be checked
within the checker’s limits is reported as counterexample.unchecked: for
quantifiers, that is a body that multiplies or divides bound variables, or
applies a spec to one at infinitely many values that matter. Nested
quantifiers over linear terms and reads, such as “every element has a larger
one”, are always checked. For an inductive predicate that the
counterexample needs false, the message names the application
(whether odd(4) holds): a proved postcondition of the predicate that
excludes those arguments lets the checker decide it. A counterexample
checked with such a postcondition rests on it: while that postcondition is
not established, the failure is reported as unresolved with reason
spec.post.
An inductive predicate is seen through its unfolding at each application a
proof names. When a verdict still depends on it, the verifier unfolds the
applications inside those unfoldings as well, one level more at a time, up
to four levels, before it reports one of the reasons above. A claim that
needs the predicate false where its derivations go round a cycle is
spec.fuel with every counterexample found needs P(...) to hold, but no
derivation shows it: every unfolding holds just as well of a predicate
true on the cycle, so no unfolding settles it, and a postcondition
!cppverify::result || Q or a lemma proved by induction is the fix.
backend.invalid-result is different: the solver’s answer contradicts the
query or a fact the verifier gave it, which is a solver fault and says
nothing about your program.
Every reason code at a glance¶
Every result that is not a proof carries one stable reason code (text
[reason=...], JSON "reason"). What it means and what to do:
Reason |
Meaning, and what to do |
|---|---|
|
A failure: the counterexample holds under the true definitions. Fix the code or the contract. |
|
The solver found a model the checker could not confirm within its
limits ( |
|
Every counterexample relies on a recursive spec or an inductive
predicate beyond what unfolding settles, the automatic inductions did
not prove the goal, and no proved fact confirms a counterexample.
Raise |
|
The proof needs a definition that is hidden ( |
|
The proof relies on a spec whose termination check did not pass. |
|
The proof relies on a spec’s postcondition that is not established. |
|
The proof relies on a spec’s |
|
The proof relies on an inductive predicate whose rules did not all hold. |
|
Proofs rest on each other without a shared measure. Give them
|
|
The proof relies on a callee contract nothing establishes. Fix the
callee, or mark a library declaration |
|
A loop has no termination measure. Add |
|
The failed obligation stands for a construct the verifier does not model; the message names it. |
|
A query, or the function’s whole budget, ran out of time. Raise
|
|
The solver gave up without a verdict, or was stopped because the
other solver of |
|
|
|
The solver (cvc5) is not installed; pass |
|
The cvc5 process could not run or printed something unexpected. |
|
Two solvers, or two queries, gave opposite decisive answers. Report it: it is a fault in a solver or in the verifier. |
|
A solver’s answer contradicts the query or a fact it was given: a solver fault, which says nothing about the program. |
|
|
|
The query is larger than |
|
The backend does not support a feature the query needs (Lean and collections, for example). |
|
The verifier could not build or encode the query. Report it. |
|
Lean export or certification could not complete. |
|
The proof cache could not be read; the proof is not taken from it. |
|
The run was stopped with Ctrl-C before the function’s verification
ended ( |
Other results that are not counterexamples:
solver.timeoutwiththe function's time (--function-timeout) is spent: every query of the function together used its budget (ten times--timeoutby default), so the remaining ones did not start. Split the function, help it with lemmas or assertions, or raise--function-timeout(0disables it).decreases.missing: a loop has no termination measure. Give itcppverify::decreases, orcppverify::decreases(*)to allow divergence, after which the function and its callers reportVerified ... [partial]: proved for the executions that terminate.callee.contract: the proof relies on a callee contract that nothing establishes: the callee’s own verification failed, or the callee has a contract but no definition. Fix the callee first; for a library function whose contract you accept, mark the declaration[[cppverify::trusted]].spec.inductive: the proof uses an inductive predicate whose rules (the generated proofs that its body and the predicate imply each other) did not all hold, so its unfolding is not a fact. The failing rule is reported on its own line, such asreach (introduction); it usually needs more--timeout.proof.cycle: the proof relies on facts whose own proofs rely on it, such as a spec’s proof block that calls a lemma proved from that spec’s postcondition. Each would hold only by the other. Give the functionscppverify::decreasesclauses of one length that every call and application between them lowers (see chapter 13), so they are proved together by induction; or prove the lemma without the spec’s postcondition, or move the reasoning into the block.construct.unsupported: the failed obligation stands for a construct the verifier does not model, so it says nothing about the program. The message names the construct, such as a pointer difference between two parameters’ objects or a call that keeps a pointer to local storage.[trusts=f]on a verified result: the proof relies on the contract off, marked[[cppverify::trusted]](see Chapter 17 — Backends, modular calls, and debugging).[vacuous]on a verified result, with a warning: no execution reaches the claim. See the next section.(its precondition is assumed, not checked, at calls from unverified g)on a verified result:gis not verified (it has no contract, assertion, ghost code, or loop contract), so nothing checks that it establishes the precondition when it calls. The proof holds for every call that does; givega contract to have its calls checked (JSONunverified_callers).
When a proof is too easy¶
An implication with a false premise is true. If no execution can reach a claim, the verifier proves the claim, whatever it says. That happens when an assumption contradicts itself or the program:
int never(int x)
cv::pre(x > 0 && x < 0) // no x satisfies this
cv::post(cv::result == 42)
{
return 0;
}
Verified: never [backend=z3] [vacuous]
warning: never: the precondition is unsatisfiable, so every claim about it
holds vacuously
return 0 “establishes” cppverify::result == 42 because no call can reach it. The
mark is there so that such a result is never read as a proof. Every
Verified result is checked at the places an assumption can enter: the
preconditions and type invariants, each behavior’s assumption, and each call
of a trusted contract, whose postcondition is assumed without proof:
[[cppverify::trusted]] int impossible(int x)
cv::post(cv::result > x && cv::result < x);
int sometimes_calls(int x)
cv::pre(x >= 0 && x <= 10)
cv::post(cv::result >= 0)
{
if (x == 3) {
int y = impossible(x); // no result satisfies the contract
return -1; // so this "satisfies" result >= 0
}
return x;
}
vacuity.cpp:1:28: Trusted: impossible (contract assumed, not verified)
Verified: sometimes_calls [backend=z3] [vacuous] [trusts=impossible]
vacuity.cpp:9:5: warning: sometimes_calls: the trusted contract of
impossible contradicts the state of this call, so everything after it
holds vacuously
A behavior whose assumption no admitted input satisfies is reported too: its postconditions are never checked.
int clipped(int x)
cv::pre(x >= 0 && x <= 10)
cv::behavior(small, x < 5)
cv::post(cv::result == x)
cv::behavior(huge, x > 20) // contradicts the precondition
cv::post(cv::result == 1000)
cv::behavior(large, x >= 5)
cv::post(cv::result == x)
cv::complete_behaviors
{
return x;
}
Verified: clipped [backend=z3]
vacuity.cpp:5:24: warning: clipped: behavior huge never applies: its
assumption contradicts the preconditions, so its postconditions are
never checked
Dead code alone is not flagged. A defensive branch such as if (p ==
nullptr) return -1; under cv::pre(p != nullptr) is unreachable for a good
reason, and every claim of the function is still checked on the paths that
remain. Only an assumption that removes the paths a claim needs is a
problem, and those are exactly the places checked above: a verified callee
returns, so ordinary calls cannot cut paths (a callee with
cppverify::decreases(*) may not, and its callers are reported [partial]).
A finding that no execution reaches a claim rests on the facts its query
uses, as a proof does. When a result is Unresolved because one of those
facts is not established, the finding is dropped where the missing fact may
be its cause:
int contradictory(int x)
cv::post(cv::result == x && cv::result == x + 1) // no result satisfies this
{
return x;
}
int relies_on_it(int x)
cv::post(cv::result == 7)
{
return contradictory(x);
}
vacuity.cpp:2:28: error: verification failed: contradictory [...::postcondition@2:28] (...) [backend=z3] [reason=counterexample]
Unresolved: relies_on_it [backend=z3] [reason=callee.contract] (relies on the contract of contradictory, which is not established)
relies_on_it proves its postcondition only because the contract of
contradictory cannot hold, which is the problem to fix, and the reason
names it. Whether no execution reaches its end is a question about that
contract too, so it is not reported. A spec whose termination fails can
contradict itself the same way, and whether the solver notices is not a fact
about the program. A finding about the entry state alone is kept when what is
missing is a callee’s contract, since no callee contract enters the entry
state: an unsatisfiable precondition is still reported, with [vacuous].
Before 0.2, relies_on_it was also reported [vacuous], with the warning
that no execution reaches its end.
Quantifiers that run away¶
A query that times out with quantifiers in it is often a matching loop: each
instance of a quantifier creates a term that triggers another instance. Run
with --profile-quantifiers to see which quantifier the solver kept
instantiating:
note: the quantifier at 43:7 was instantiated 2914 times, up to generation 41
The fix is usually a better trigger. cppverify::trigger(term) inside a quantifier body
makes term the pattern that instantiates it (see
Chapter 13 — Spec and proof functions); choose a term that the instance does not
recreate at a larger argument.
A requested proof cache is also fail-closed. cache.corrupt means an entry
did not exactly match its semantic/backend identity; cache.io-failed means a
requested entry could not be read. Neither is treated as a cache miss or a
proof. A failure to store a fresh proof is reported as cache-error telemetry
and does not erase that proof, and a failure to prune the cache once the run
has finished is a warning (cannot prune the proof cache: ...). Text diagnostics report
cache hits/queries, while JSON includes cache.hits, cache.misses, and
cache.errors.
For a deliberately invalid program, two outcomes are sound:
error: verification failedmeans a counterexample was found and checked;Unresolved(with a reason code) means the verifier conservatively refused to certify it.
Only Verified, Certified, and Proved (z3+lean) (Proved
(portfolio+lean) after the strict portfolio) are proof results.
Lowered is not a fourth solver outcome. It is emitted only by
cpp-verify --lower-only and says that Clang AST conversion, VCR, passive
SSA, canonical Obligation IR generation, and backend encoding succeeded without running
satisfiability. This is useful when isolating a frontend or lowering bug from a
slow quantified/heap query, but it never certifies the program.
Exported is also not a proof result. A standalone Lean scratch-pad contains
sorry. An editable project separates generated semantics from preserved
user proofs, but its initial proof files are still admitted. Only
--lean-certify compiling every active proof under the pinned toolchain,
with no sorry or undocumented proof axiom, reports Certified.
For automation that remains unresolved, use:
cpp-verify --lean-fallback=proof file.cpp
# edit proof/CppVerify/User.lean and proof/CppVerify/Proofs/*/*.lean
cpp-verify --lean-fallback=proof --lean-certify file.cpp
The first command remains non-success because export is not proof. It exports
only the obligations Z3 did not prove and reports the split, for example
[z3: 18 of 20 proved; lean: 2 exported]. Once those proofs kernel-check,
the function is Proved (z3+lean), or Certified when Lean checked every
obligation (with --lean-fallback-scope=all, or when the solver proved none
on its own); it is never Verified. A Z3 counterexample is not routed
through this fallback.
Trusting a new feature¶
Do not use one successful Z3 result as the only implementation oracle. A feature regression should combine:
realistic C++ programs that must verify;
nearby false programs that must be rejected;
exact VCR and passive-SSA checks;
critical typed Obligation IR and Z3-encoding checks;
boundary cases for mathematical integers and machine integers.
This split answers two independent questions. --lower-only checks whether
the program became the intended formula. Ordinary verification checks whether
that formula is valid. A timeout can block the second answer without hiding a
malformed first answer.
Proof failure versus unsupported C++¶
A source program can also be outside CppVerify’s current semantic subset. That is different from a failed proof:
a conversion/unsupported error means the relevant C++ semantics are not modeled and verification stopped fail-closed;
verification failed means the semantics were lowered and a counterexample violates an obligation;
Unresolved means the obligation was lowered but automation did not decide it; its reason code says why.
Do not work around an unsupported diagnostic by replacing a C++ operation with an unchecked integer or external axiom. Either reformulate the program within the documented subset or add the missing semantics through Clang, VCR, passivization, backend encoding, and positive/false-proof tests.
The full feature matrix, memory/object-model boundaries, missing induction and solver tactics, performance work, library models, and raw-C++ readiness gates are maintained in Current limitations and C++ readiness.
Adjusting contracts¶
Situation |
Response |
|---|---|
Precondition too weak |
Strengthen |
Postcondition too strong |
Weaken |
Loop invariant too weak |
Add facts to |
Spec vs machine integers disagree |
Use |
Recursive specification |
Use the smallest sufficient |
Pointer aliasing |
Prove distinct pointers or declare |
Overflow / divide-by-zero |
Add the precondition the counterexample points to (see Chapter 18 — Undefined behavior) |
Indexed access or pointer step is out of bounds ( |
Declare the correct |
Heap fact disappears after a call |
Give the callee a |
Heap fact disappears after a loop |
Add a loop |
Further reference: Language reference.