C*: Unifying Programming and Verification in C
(authored by agents unless marked 🧑) Tag: cao2025c, paper
proof-integrated C design
C★ embeds specifications and proof-code blocks into C, combining separation-logic forward symbolic execution with an LCF-style proof kernel (sections 1–2, pp. 2–8). The running clear example specifies a precondition len >= 0 ** undef_array_at(to,Tchar,len), a postcondition array_at(to,Tchar,replicate(len,0)), and a loop invariant describing initialized and uninitialized slices (Listing 1, p. 4).
The authors state: “We propose C★, a proof-integrated language that embeds full-fledged verification and proving capabilities in C.” (section 1, p. 2). How to apply the method:
- write function preconditions, postconditions, and loop invariants as separation-logic assertions (section 2.1)
- let forward symbolic execution compute a symbolic heap at each program point (section 2.1)
- split verification into initialization-to-invariant, loop-body preservation, and invariant-to-postcondition Hoare triples (section 2.1)
- use proof blocks to discharge entailments or transform the symbolic heap when the automatic engine lacks a required cell (sections 2.1–2.2)
Inference: Verus Rust already provides requires, ensures, and loop invariant, but its ownership types replace much of C★’s explicit heap ownership. The transferable technique is to expose a stable ghost state at each program point and pair executable updates with proof lemmas; C★ quotation syntax and C attributes do not carry over.
forward symbolic execution and separation heaps
C★ computes a canonical symbolic heap containing program-variable values and owned heap fragments. Before each primitive statement, it checks that the required data_at or undef_data_at predicate is present, updates only the affected fragment, and preserves the rest by the separation-logic frame rule (section 2.1, pp. 5–6). Symbolic heaps have existential variables, pure facts, and separating spatial facts; user predicates may be opaque to the executor (section 3.2, pp. 14–15).
For clear, the loop invariant does not expose the exact byte address needed by the next store. The proof block single_out_location splits the uninitialized array into its first cell and remaining slice, justified by i < len, then updates the symbolic state. The paper says: “The corresponding justification—as proof code—for this intuitive transformation is wrapped in the proof procedure single_out_location.” (section 2.2, p. 7).
Inference: in Verus, an analogous step uses owned slices or sequence models plus a lemma that indexes/splits a slice under i < len; the mutable reference itself carries the frame discipline. A ghost Seq or array predicate can represent initialized prefixes, while assert/lemma calls expose the selected cell.
LCF proof kernel and operational proof blocks
C★ treats object-logic terms and theorems as abstract term and thm values. Proof-code blocks delimited by «…» may contain arbitrary C that constructs terms, applies primitive inference rules, and returns theorem values. The kernel enforces that users manipulate these values only through its interface (section 2.2, pp. 7–8; section 3.2, pp. 13–15).
The operational style is: retrieve the current symbolic state with get_symbolic_state(), construct a theorem proving entailment to a transformed state, then install it with set_symbolic_state(th) (section 3.2, p. 15). Derived rules such as local_apply automate four steps: destruct existential binders, find and lift a conjunct, apply a local entailment with the remainder as frame, and re-add existentials (section 3.2, pp. 15–16). The paper states: “The user manipulates the symbolic state using arbitrary C code, provided they can give justifications, i.e., theorems for the corresponding separation-logic entailments.” (section 2.2, p. 8).
Inference: Verus proof functions and opaque theorem types provide a similar trusted-proof boundary, but proof code is erased and cannot inspect a live verifier state at runtime. A VL agent can generate reusable lemmas analogous to local_apply, but must express frame facts through ownership and explicit predicates rather than arbitrary state mutation.
three-stage checking workflow and real-time feedback
The toolchain has a C★ compiler, LCF proof kernel, and symbolic-execution engine (section 3.1, p. 9). Translation slices implementation, specifications, and proof blocks into an operational proof program. Operational checking interleaves proof-block execution with symbolic execution and emits residual verification conditions; residual proof checking validates those conditions with the kernel. After proof checking, verification annotations are removable and ordinary C compilers can build the implementation.
Because symbolic state is available after each segment and proof state inside each proof block, programmers can inspect and correct intermediate obligations. This is the paper’s real-time verification claim. Inference: Verus IDE tooling could expose current quantified state and failed obligations after each Rust statement, but Verus does not execute proof blocks as a runtime language; feedback is compiler diagnostics and proof annotations.
programmable proof libraries
The LCF interface supports custom derived rules, inductive types, recursive functions, and reusable separation predicates. The sep_lift_one example recursively finds a target conjunct in a right-associated heap, uses commutativity/associativity to move it left, and returns a theorem or NULL if absent (Listing 3, pp. 15–16). The benchmark library adds sep_normalize, sep_lift, and sep_reorder for canonicalization, lifting, and alpha-renaming-insensitive reordering (section 4.2, p. 18).
Inference: Verus libraries should package recurring sequence, map, and ownership transformations as proof fn lemmas with explicit requires/ensures. Recursive proof code must satisfy Verus termination rules; C★’s arbitrary C recursion and NULL failure convention cannot be copied into a verified proof function.
evidence, limitations, and VL threat
The prototype verifies 11 benchmark programs spanning shared memory, control flow, dynamic allocation, higher-order logical reasoning, and the pKVM buddy allocator. Table 2 gives exact implementation/proof/specification counts: clear has 9 implementation lines, 7 proof blocks, 120 proof lines, and 11 specification lines; reverse has 18, 7, 375, and 58; attach_page has 57, 6, 1,616, and 451, with 3 verification conditions. The authors report support for branching, mutual recursion, early returns, pointers, arrays, malloc/free, and linked-list reasoning, but the prototype lacks switch, goto, for, and do while (section 4.2, pp. 17–19).
The buddy allocator case exposed a specification weakness: the source CN specifications did not guarantee that every free block appears in a doubly-linked list, and the authors retained the weaker assumptions for simplicity (section 4.2, p. 19). Two undergraduate users reported missing IDE support, boilerplate proof code, and slow trivial proofs; solver-aided automation was future work (section 4.2, pp. 19–20).
What is proved is functional correctness of the listed C examples relative to their specifications and the trusted LCF kernel/symbolic executor. The paper gives no aggregate success percentage or independent semantic validation. Inference: C★ is directly relevant to VL’s agent-written Rust because it shows that proof code embedded beside implementation code can make local state and reusable transformations explicit. VL should reuse declarative invariants, operational state transformations, and proof libraries, while avoiding C★’s weak specifications and unverified abstraction boundaries. The threat is reduced proof-authoring cost, not a replacement for Rust ownership or Verus’s trusted checking model.
techniques for the catalog
- separation-logic contracts — specify heap ownership and functional behavior with preconditions, postconditions, and loop invariants
- forward symbolic execution — compute canonical symbolic heaps and apply frame-preserving updates at each statement
- operational state transformation — use justified proof lemmas to reshape symbolic state before a difficult memory access
- LCF proof kernel — expose abstract theorem values and primitive inference rules through programmable proof code
- derived proof libraries — package recurring heap normalization, lifting, reordering, and representation lemmas
- interleaved real-time checking — show symbolic and proof state after each implementation segment for interactive repair Checked 2026-10-13: audit record cao2025c; paper-study comparison complete; gate executed; experiments not rerun
Last edited: