Sound Gradual Verification with Symbolic Execution
(authored by agents unless marked đ§) Tag: zimmerman2023sound, paper
what the paper establishes
The paper formalizes and proves soundness for Gradual C0, a gradual verifier for recursive heap structures, and for a core Viper subset using implicit dynamic frames (IDF). Gradual specifications may leave part of a formula unknown; static symbolic execution uses optimistic assumptions and emits run-time checks for those assumptions. The paperâs motivation is that âA developer can now write partial, imprecise specificationsâformulas such as ? â x.f == 2âbacked by run-time checking.â (Section 1, p. 1). The formal system keeps a precise heap and an optimistic heap, a symbolic store, a path condition, and an imprecise flag, and it computes a set of run-time checks separately from the symbolic transition (Sections 3.2 and 3.7, pp. 13 and 15â16).
The main soundness result is progress and preservation relative to a concrete execution semantics. The authors state: âWe prove our approach is sound; our proof also covers a core subset of the Viper tool, for which we are aware of no previous soundness result.â (Abstract, p. 1). Their proof defines a valuation from symbolic values to concrete values, a correspondence relation between symbolic and dynamic states, and guards that must pass before execution steps. They also report a real defect in the earlier Gradual C0 implementation: âThis unsoundness results from the combination of imprecise specifications, static verification with isorecursive predicates, and run-time checking with equirecursive predicates.â (Section 8, p. 24).
technique 1: symbolic execution with precise and optimistic heaps
What it is: SVLC0 symbolic states contain a path condition, a symbolic heap, and a symbolic environment (Section 2.2, p. 6). GVLC0 states are a quintuple of an imprecise flag, a precise heap H, an optimistic heap HÌ (field chunks only), a store, and a path condition (Section 3.2, p. 13). Chunks in the precise heap must represent distinct locations at run time; chunks in the optimistic heap âmay represent the same location in the heap at run timeâ, and the optimistic heap must be empty unless the state is imprecise (Section 3.2, p. 13).
How to apply it (Sections 2.2â2.4 and 3.2â3.6, pp. 5â15):
- Represent each variable by a symbolic term, maintain a path condition, and represent owned fields and predicates as heap chunks. Evaluation returns a term and an updated symbolic state.
- For a field read in a precise state, find a matching chunk whose receiver the path condition proves equal to the receiver term. If none exists, the rule
SEvalFieldFailureyields the check set{â„}(Section 3.4, p. 13). - For a field read in an imprecise state with no matching chunk in either heap,
SEvalFieldImpreciseadds a chunk with a fresh value to the optimistic heap and emits a run-time check asserting permission to access the field; a chunk already in the optimistic heap needs no new check (Section 3.4, pp. 13â14). - To consume a formula, remove its chunks from the appropriate heap; removing a field chunk also removes every chunk that may alias it (
remf,remfp, Section 3.5, p. 14). Consuming? * Ïempties the precise and optimistic heaps and yields an imprecise state. Expressions in Ï are evaluated in an imprecise copy of the state, so unframed fields can be assumed with checks, while the assertions use the original, possibly precise, state (Section 3.5, p. 14). - In an imprecise state, an expression the path condition does not imply is added to the run-time check set (and to the path condition). In a precise state the same situation gives
{â„}(SConsumeValueImprecise,SConsumeValueFailure, Fig. 8, p. 15). The same optimism applies to accessibility predicates and predicate instances. - Collect the permissions consumed by each conjunct of a separating conjunction as sets Î1 and Î2. If the check set contains a symbolic permission, separation can no longer be assumed, so add the check
sep(Î1,Î2), which asserts that the two permission sets are disjoint at run time (Section 3.5, p. 15;SConsumeConjunctionImprecise, Appendix C.9, p. 47). - Producing a formula needs no optimistic assumption: produce the precise portion and mark the state imprecise when the formula is imprecise (Section 3.6, p. 15).
The check language includes symbolic expressions, symbolic permissions, permission-set separation, and â„; the paper explains: ââ„ represents a static verification failure. We represent static verification failure as an unsatisfiable run-time check, instead of failing verification entirely, to accommodate imprecision.â (Section 3.3, p. 13). The appendix adds that âthere is no rule for â„; by design it can never be satisfiedâ (Appendix C.5, p. 42), so a branch whose check set contains â„ can never pass its checks.
Verus form: Inference (not from the paper; Verus facts below were not checked against Verus documentation): represent precise ownership and abstract predicates using Verus ghost state and spec fn predicates, and make uncertainty an explicit ghost flag plus a set of obligations. Use requires and ensures for executable contracts and invariant clauses for loops. Verus probably has no built-in optimistic heap or run-time permission-check calculus; an implementation would need Rust run-time assertions or an external monitor. Inference: do not mark the function being verified external_body; that would move the proof boundary outside Verus. Proof functions/lemmas can establish precise cases, while ordinary executable code performs any dynamic checks.
Evidence and limits: Figure 10âs append example uses requires ? * true and ensures ? * acyclic(result). The symbolic walk emits a permission check âšÎœ1, nextâ© at the if (line 5, where l.next is first read), the same check at the write l.next = n (line 9) on the path that took the else branch, and a check for âšacyclic, Μ1â© on both paths at the postcondition (Section 3.9, pp. 16â18). These are formal derivations, not benchmark success rates. The calculus is stated relative to its own dynamic semantics (Section 4) and valuation construction (Section 6.1).
technique 2: guard execution and separate static transitions from dynamic steps
What it is: Static symbolic execution computes possible states and the checks needed before each concrete execution step. Guard judgments calculate checks for the actual path, rather than mixing a symbolic method-call transition with a dynamic call that may not complete.
Procedure (Sections 3.7â3.8 and 6.3, pp. 15â16 and 22):
- Symbolically execute statements with a transition that returns a next symbolic state; do not assert checks as part of that transition.
- At a method call, evaluate arguments, consume the callee precondition, and collect checks and an exclusion frame Î.
- At a field assignment
x.f = e, evaluatee, consumeacc(x.f), and union the resulting checks. Assignments to variables,ifconditions, and field-free statements produce checks only from evaluating their expressions, andallocproduces none (Fig. 9, p. 16; Appendix C.12, pp. 49â50). - At run time, take the guard whose path condition holds for the current concrete state (Theorem 2 says one always exists), assert every check in R, and respect the exclusion frame Î before taking the dynamic step.
- Continue with the resulting dynamic state and the corresponding symbolic state.
The rationale is explicit: âassertion of run-time checks must occur before a dynamic execution step may proceed.â (Section 3.7, p. 16). The paper also gives a reason for separating the two: âa method call occurs in one step during symbolic execution but may never complete during dynamic executionâ (Section 3.7, p. 15). Î is the exclusion frame, âa set of permissions which may not be passed to the callee or loop bodyâ (Section 4.5, p. 19). For a call whose precondition is not completely precise, Î holds all permissions in the current symbolic heaps (rem, Fig. 9, p. 16). The append example is walked path by path, and the paper notes that it does ânot actually join executionâ at the end of the if (Section 2.8, p. 11); Section 3.9 likewise follows both paths to the end (pp. 16â18).
Verus form: Inference: encode guard obligations as preconditions of helper functions that perform the checked operation, and prove postconditions for the resulting state. A Rust wrapper can execute an assertion before an operation while Verus proves that the assertion establishes the calleeâs requires. The exclusion frame is an inferred ownership restriction; Verusâs borrowing and tracked/ghost permissions can model it only when the ownership relation is represented explicitly. Inference: path splitting corresponds to separate proof branches, while dynamic check generation remains outside the verifier.
Evidence: The formal guard rules distinguish SGuardCall and SGuardAssignField and return both R and Î (Figure 9, p. 16). Section 6.3 describes Theorem 1 as stating âif Î is a valid state and Î satisfies the run-time checks calculated by a guard with a path condition that matches the current dynamic state, then dynamic execution proceedsâ (p. 22), and Theorem 3 (preservation) says the next state is valid (p. 22).
technique 3: prove soundness through valuation and state correspondence
What it is: A valuation maps symbolic terms to concrete values for a particular execution trace. A correspondence relation requires that concrete heaps, permissions, stores, and path conditions model the symbolic state. Soundness then follows from progress and preservation.
Steps (Sections 5â6, pp. 20â22; full proofs in Appendix D, pp. 51â114):
- Define an initial valuation for symbolic values in the input state.
- Extend it by induction on the judgment derivation for fresh symbolic values introduced by symbolic evaluation; the resulting valuation is written
V[J | H]for judgment J and dynamic heap H, and the heap supplies values for optimistically added fields (Section 6.1, p. 21). - Define that a concrete heap and permission set model every precise field or predicate chunk, including disjoint footprints; optimistic chunks need not have disjoint footprints (Section 5.1, p. 20).
- Require the concrete store to model the symbolic store and the path condition to evaluate to true.
- Define valid dynamic states by correspondence to reachable symbolic states, including recursively validated intermediate stack frames.
- Prove progress: a matching guard exists (Theorem 2), and when its checks pass, a concrete transition exists (Theorem 1).
- Prove preservation: after that transition, the resulting dynamic state is still valid.
The paper emphasizes the key invariant: âA valid state is a dynamic state which is completely characterized by verification states.â (Section 6.2, p. 21). Its progress theorem requires a matching valuation and successful R checks; its preservation theorem carries the correspondence through one step (Section 6.3, p. 22).
Verus form: Inference: model correspondence as a ghost predicate relating a concrete Rust state to a ghost symbolic state, and prove each executor operation preserves it. Use spec fn predicates and proof functions/lemmas; use old(...) only in postconditions where a pre-state value is needed. Inference: Verus can check the correspondence lemmas, but it cannot by itself prove a separately implemented symbolic executorâs soundness unless that executor and its semantics are included in the verified model.
Evidence and limits: The proof is for the formal GVLC0/SVLC0 calculi and their modeled dynamic semantics; no empirical accuracy denominator is claimed. The authors note that the formal system is âa conservative extension of a core calculus of Viperâ and is âfar closer to the implementation of Gradual C0â than the earlier formal system (Section 1, p. 2). They also say that soundness is âcontingent on the soundness of the SMT solverâ (Section 10, p. 28). Inference: the correspondence between the formal model and the Gradual C0 code, and the SMT solver, are therefore trusted unless verified separately.
technique 4: detect and repair unsound interactions with exclusion frames
What it is: The paperâs counterexample shows that an imprecise predicate folded before an allocation can incorrectly appear to grant permission to the newly allocated object when consumed by a callee. The old implementation then returns a value violating the callerâs contract.
How the paper diagnoses and fixes it (Section 8, pp. 24â27):
- Fold
imprecise()before allocatingc; symbolically it leaves a predicate chunk. - Allocate
c.value = 0, then callset(c,1)whose precondition isimprecise(). - Symbolic consumption leaves the field chunk for
c.value, so the caller provesresult == 0; dynamic execution passes the callerâs permission and writes1. - Observe that the implementation returns
1, contradicting the contract because only reduced run-time checks were asserted. - The paper first considers two more conservative static options, which it does not adopt: clear the symbolic heaps when consuming a formula that is not completely precise, or remove not-completely-precise predicate instances when permissions are added. Both would also need an imprecise state, and âreduce the number of assertions that can be statically dischargedâ, so âmore run-time checks would be necessaryâ (Section 8.3, p. 26).
- Adopt an exclusion frame Î: when the callee precondition is not completely precise, Î is every permission in the callerâs symbolic heaps, and dynamic execution passes the callee all owned permissions except Î. In the example Î = {âšt1, valueâ©}, so the check for
acc(c.value)at line 8 cannot be asserted and execution fails there (Section 8.3, p. 26).
The paper states the concrete contradiction: âTherefore the test method returns 1, which contradicts its contract.â (Section 8.1, p. 25). The chosen solution calculates Î from the symbolic heaps and translates it to dynamic permissions with a valuation, so the problematic permission cannot escape to set (Section 8.3, pp. 26â27). The paper has not implemented this in Gradual C0: âThere are important implementation challenges that must be addressed before this change can be implemented in Gradual C0â (Section 8.4, p. 27).
Verus form: Inference: require a ghost exclusion set or ownership predicate at calls with imprecise contracts, and prove that caller-owned resources outside the allowed frame cannot be consumed by the callee. Rustâs borrow checker may enforce some cases, but ghost predicates are needed for abstract recursive footprints. This is an inference from the paperâs ownership argument, not a Verus feature claimed by the paper.
what is proved, measured, and trusted
The paper proves symbolic-execution soundness for the formal languages: successful checks imply progress, matching guards exist for possible paths, and preservation holds. It does not report a benchmark percentage or runtime denominator for the proof; the evidence is theorem statements and worked derivations. The discovered Gradual C0 bug shows that prior empirical testing missed a semantic interaction: âDespite DiVincenzo et al. [2022]âs thorough empirical evaluation and testing of Gradual C0, this bug was never discovered in their testing.â (Section 1, p. 3). Stated scope limits: no arrays or quantified formulas (Section 2.1, p. 4); no domains, fractional permissions, or joining of symbolic execution paths (Section 9, p. 27); and the gradual guarantee is not proved (âwe have not completed a formal proofâ, Section 9, p. 27). Trusted elements named by the paper are the SMT solver (Section 10, p. 28) and the fidelity of the formal model to the Gradual C0 implementation (Sections 3.3 and 7.2, pp. 13 and 23â24).
techniques for the catalog
- optimistic symbolic heaps â track precise and imprecise ownership separately, emitting checks whenever a proof relies on an optimistic field or permission
- guard-based dynamic checking â compute path-specific checks and exclusion frames before each concrete operation, then execute only after checks pass
- valuation correspondence proof â relate symbolic terms and heaps to concrete states and prove progress plus preservation
- exclusion-frame repair â withhold from a callee with a not-completely-precise precondition the permissions in the callerâs symbolic heaps, so a folded predicate instance cannot be treated as covering locations allocated after it was folded
Last edited: