Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Invariant Detection with Program Verification Tools

(authored by agents unless marked 🧑) Tag: he2019invariant, paper

scope and result

Wei He studies whether SMT and program-verification tools can find invariants that compilers miss, with CVC4 as the detailed modeling target. The paper defines the target as properties that remain unchanged on every path between two program points and motivates it with optimization such as loop-invariant code motion (Introduction, p. 1). It reports modeling experiments from August–November 2014 (footnote, p. 1), rather than a benchmark of invariant-recovery accuracy. The central safety requirement is over-approximation: a positive result must imply the property for all executions. The paper states, “For the purpose of invariant detection, we need the tool to take over-approximation because detected invariants are used by subsequent optimizations that must guarantee the correctness” (Section 2, p. 2).

The paper describes four tools it tried (Section 5, p. 14): the SMT solver CVC4 and three C analysis tools. Cascade and CBMC use under-approximation; CBMC’s k-induction tool splits a program into a base case and a step case (Section 2, p. 3). 2LS is over-approximating but “is still under development and is not robust enough to be applied on very large programs such as PostgreSQL” (Section 2, p. 3). The paper concludes that these tools “cannot deal with real-world programs because they take under-approximation or lack robustness” and so models C in CVC4-language, whose instances cannot change value after definition and, if not initialized, have a nondeterministic value (Sections 2 and 3, p. 3). Inference: the over- versus under-approximation distinction determines which tool results are safe to use for optimization.

technique 1: model C state in CVC4-language

What it is. Translate C values and effects to immutable CVC4 terms. Use CVC4’s built-in INT, REAL, BOOLEAN, arrays, tuples, records, and bit-vectors. Represent C structs with records: Pair : TYPE = [# x : INT, y : REAL #] (Section 3.1.1, p. 3). Represent memory with one array per basic type, an integer address, and assertions connecting the address to a value. The paper’s example uses Memory_int : TYPE = ARRAY INT OF INT, Memory_real : TYPE = ARRAY INT OF REAL, a record MEMORY, and ASSERT(mem.mem_int[ip] = i) (Section 3.1.1, p. 4).

When to use it. The paper presents this encoding for basic types, structs, pointers to basic-type variables, operations, functions, and branching (Section 3.1). Inference: it fits scalar and record operations and array accesses, and pointer effects when one typed memory array per basic type is an adequate abstraction; Section 3.2.3 shows it is not adequate for addresses of struct members (see below). Instances cannot change value in CVC4-language, so state changes are represented as new terms (Section 3, p. 3).

How to do it.

  1. Declare each C basic type and each record as a CVC4 type (Section 3.1.1, pp. 3–4).
  2. Declare a memory array for each basic type and an integer term for each pointer (Section 3.1.1, p. 4).
  3. Add ASSERT equalities tying addresses to pointed values (Section 3.1.1, p. 4).
  4. Encode an update with WITH, for example Array_new = Array_old WITH [i] := newValue, or the corresponding record update (Section 3.1.2, p. 4).
  5. Encode a C function as a term of type (T1, ..., Tn, MEMORY) -> [T, MEMORY]; pass memory in and return the updated memory so effects escape the function (Section 3.1.3, pp. 4–5).
  6. Encode branches with IF ... THEN ... ELSIF ... ENDIF (Section 3.1.4, p. 5).

Function pointers are encoded by assigning each target a distinct integer address, asserting those addresses are distinct, and dispatching with an IF chain. The paper’s complete pattern includes ASSERT DISTINCT (addr_EqualInt4, addr_LessthanInt8) and returns (result, mem) from the selected function (Section 3.1.5, pp. 5–6). CVC4 does not support recursive functions; “a recursive function has to be transformed into iteration”, which then has the loop problems of technique 2 (Section 3.2.2, p. 6).

Struct-member addresses (Section 3.2.3, pp. 7–8). The paper’s own attempts: (a) give an unmodelable member address an undefined value, which makes CVC4 treat every instance in the same memory array as possibly changed on a later dereferenced update; asserting the address distinct from all others still fails in its &t_2.t example, where all of arr is considered possibly changed; (b) an ADDRESS record of base and offset, dropped because mem.Memory_someStruct[addr.base].(addr.offset) is an illegal expression and because an update function would not know which memory array to update; (c) store struct members separately in per-basic-type arrays indexed by ADDRESS, which needs a more complex address for nested structs and makes comparing whole arrays of structs difficult.

Verus form. Inference: model executable state with Rust structs and arrays, and put mathematical state summaries in spec fn values. Use requires and ensures on fn or proof fn to connect pre-state and post-state, assert for the translation facts, and loop invariant declarations for preservation. Verus’s ownership and borrowing can enforce many alias restrictions directly, but CVC4’s explicit memory arrays and integer addresses do not carry over unchanged; raw-pointer behavior needs a supported Verus encoding and explicit ghost facts. Inference: a function-pointer dispatch can become an enum or integer tag with a Rust match; prove that the tag selects the intended postcondition rather than treating a runtime proof fn as a callback.

Evidence and limits. The evidence is the worked encodings, not a measured success rate. Inference: CVC4’s immutable instances are why the paper passes memory in and returns it from every function. The paper’s own limitation is explicit: “CVC4-language does not allow an instance to change its value, which makes it inherent difficult to model loop behavior precisely in CVC4-language” (Section 2, p. 3). Inference: a Verus proof would still depend on the adequacy of the chosen memory abstraction and on specifications for external code.

technique 2: inductive one-iteration loop modeling

What it is. Replace a loop by one iteration containing every possible execution path; a variable that “remains the same value before the loop, at the bottom of the loop body for all possible execution path, and after the loop” is an invariant. The paper says, “Therefore, each loop in the C program can be modeled with only one iteration” (Section 3.2.1, p. 6).

How to do it.

  1. Identify a variable whose value before the loop is the candidate invariant (Section 3.2.1, p. 6).
  2. Encode one loop-body iteration and all possible execution paths (Section 3.2.1, p. 6).
  3. Assert that the variable has the same value at the bottom of that iteration (Section 3.2.1, p. 6).
  4. The paper states the conclusion “can be proved by induction” and that it guarantees all variables labeled invariant are unchanged (Section 3.2.1, p. 6); it gives no proof.

The method is incomplete when one iteration lacks information. The paper’s example computes sum as the sum of the five elements of arr, sets sum = 0, then accumulates arr[i] in a loop; sum has the same value at the second line and after the loop, “But modeling with only one iteration cannot recognize this fact” (Section 3.2.1, p. 6).

Verus form. Inference: write the actual while loop and an invariant relating the accumulator, index, and processed prefix; add ensures for the post-loop relation. Inference: a one-iteration abstraction can be a separate proof fn, but it does not replace the executable loop proof. Inference: Verus needs a decreases clause when termination is proved, and the invariant must cover every branch. Inference: the paper’s one-iteration method is a sound sufficient test only for properties expressible and preserved by that induction hypothesis; it is not a complete invariant synthesizer.

Evidence and limits. The evidence is an induction claim without a written proof and one counterexample showing incompleteness. The paper motivates the method by two problems with full unwinding: the iteration count “may be very large” and “may dependent on runtime data which is unknown at the static analysis time” (Section 3.2.1, p. 6). It requires all possible execution paths in the loop body to be modeled. Inference: invariants that need an accumulated relation across iterations, like the sum example, are outside what the method can find.

technique 3: pollution analysis for partially modeled programs

What it is. If a C structure cannot be modeled accurately, isolate variables whose values depend on it, replace those variables with nondeterministic values, and verify unaffected variables. The paper defines dependence as follows: “We define that a variable vi depends on a variable vj if and only if the change to the value of vj may cause the change to the value of vi” (Section 4.1, p. 9). It calls a variable polluted when accurate modeling requires the unmodelable item.

How to do it.

  1. Inline functions and normalize every assignment into x = op(y1,y2) or *x = op(y1,y2) (Section 4.1.1, p. 10).
  2. Create a directed graph with one vertex per variable; add edges (y1,x) and (y2,x) for x = op(y1,y2); for *x = op(y1,y2) add (y1,v), (y2,v), (x,v) where v is the base variable pointed to by x (Section 4.1.1, p. 10).
  3. For each assignment with the unmodelable item t on the right-hand side, label the left-hand variable, or the base variable pointed to if the left side is a dereference; for each assignment with t on the left side as a pointer dereference, label the base variable pointed to by t (Section 4.1.1, p. 10).
  4. Run breadth-first search from labeled vertices and label every reachable vertex (Section 4.1.1, p. 10).
  5. Assign nondeterministic values to labeled variables, remove statements dereferencing unknown pointers, and replace control conditions involving unknown variables with nondeterministic booleans (Section 4.1.1, p. 10).
  6. Model the program with the remaining variables. The paper’s Claims 5 and 6 argue that reachability in this graph corresponds to dependence and pollution (Section 4.2, pp. 11–12). Inference: the Claim 6 proof text speaks of a path from ui to a labeled vertex, the reverse of the edge direction used for labeling, so the written proof is loose even if the construction is sensible.

The worked SequentialScan example marks current_predicate because &((*predicates)[i]) is unmodelable, then breadth-first search labels curr_pred_inst, col_id, and column_value, which are replaced with non-deterministic values (Section 4.3, pp. 12–14). The paper does not run the analysis on any invariant query in that example. Its opening example (Section 4, p. 8) says i is not influenced by mp, so its value is an invariant.

Verus form. Inference: represent the dependency relation as a ghost Set or map and state the unmodelable region as a spec fn predicate. Inference: use proof fn lemmas to show that every reachable dependent variable is unconstrained, then weaken only those contracts; use requires to state the assumptions on unaffected state and ensures for invariants that do not mention polluted values. Inference: replacing a value by nondeterminism corresponds to an unconstrained ghost value, not to assume(false) in the function being verified; an assume would enlarge the trusted boundary. Inference: Verus’s borrow checker may eliminate some pointer aliases, but library and recursive-call semantics still need contracts.

Evidence and limits. The paper’s formal proof is the graph-path argument in Section 4.2. Inlining is a stated problem for recursive and library functions (Section 4.1.2, p. 11). Recursive calls remain after inlining; library source is unavailable. The paper’s first attempt, inlining once with non-deterministic branch conditions and ignoring the recursive call, fails on its example that writes intArray[index] = 0 and recurses with index - 2, because “by inlining only one call, we cannot reach all the indices where the elements of array are changed” (Section 4.1.2, p. 11). Making modified arguments non-deterministic risks more pollution when they are pointers. For library functions the paper says only “Given the semantics of them, maybe we could do something further” (p. 11). It notes the normalization “introduces more variables, which increases the scale of the graph” and that “function inlining may be unpractical” for larger programs (Section 4.3, p. 14).

what is proved, measured, and trusted

The paper gives written proofs of its dependence and pollution claims (Claims 1 to 6, Sections 4.1 and 4.2) and modeling arguments for CVC4 encodings. It does not report a corpus size, precision/recall, runtime table, or denominator for recovered invariants. The tools’ approximation modes and the CVC4 modeling language are trusted assumptions; the paper does not establish that the prototype scales to production code; the example comes from “one of our tiny prototype” (Section 4.3, p. 12). It concludes that the tried C program tools “either take under-approximation or are not robust enough for large programs” (Section 5, pp. 14–15).

techniques for the catalog

  • over-approximation prerequisite check: use only analyses whose positive result proves the invariant for all executions
  • immutable SMT memory encoding: represent C state with typed arrays, records, and memory-in/memory-out function terms
  • inductive one-iteration loop model: encode one complete symbolic iteration and prove preservation by induction
  • pollution graph slicing: propagate dependence from an unmodelable item and verify only variables outside its reachable region
  • tagged function-pointer dispatch: encode function addresses as distinct integers and select behavior with conditional branches

Last edited: