CBMC: The C Bounded Model Checker
(authored by agents unless marked đ§) Tag: kroening2023cbmc, paper
bounded model checking method
CBMC translates C programs, assertions, and bounded loop executions into a bit-precise Boolean or SMT formula. âIf the formula is satisfiable, then an execution leading to a violated assertion exists.â (Section 1, p. 1) If the formula is unsatisfiable, no assertion is violated within the chosen unwind bound. The approach is precise for fixed-width arithmetic and useful for bug finding, safety checking, test generation, vulnerability detection, equivalence checking, and synthesis (Section 1, p. 1).
Use it when a Rust or C property can be checked over a finite execution bound. The complete pipeline is: parse and type-check source; convert it to guarded GOTO; instrument properties; symbolically execute and unwind loops into static single assignment (SSA); encode the equation as CNF or SMT; call a solver; decode a satisfying model as a trace (Section 5, pp. 19â26).
technique: assertions and built-in property checks
Put assert or __CPROVER_assert(condition, "description") at the property boundary. CBMC can also synthesize checks for signed overflow, division by zero, pointer and array bounds, memory leaks, and related properties (Table 1, p. 2; Section 3.2, pp. 10â11). The paperâs absolute-value example initially reports âVERIFICATION SUCCESSFULâ with zero verification conditions, which is vacuous because no assertion exists (Listing 2 and discussion, pp. 3â4). Running cbmc --function abs --signed-overflow-check abs.c generates one VCC; SAT finds a model and reports overflow at -x for input â2,147,483,648 (Listing 3, p. 4).
Steps: select the entry function with --function; enable the desired checks; inspect the generated property list with --show-properties; then run the solver. A satisfiable result is a concrete counterexample, not merely an abstract warning. --trace prints assignments and input values needed to reach the violation (Section 3.2, pp. 4â5).
Inference: in Verus, express the same obligation as requires/ensures and use executable test harnesses for counterexamples. Verusâs contracts are not CBMC assertions: a Rust contract is proved for all executions accepted by the verifier, while CBMCâs bounded SAT result is finite unless loop completeness is separately established.
technique: loop unwinding with soundness controls
CBMC replaces a loop by nested conditional copies of its body. For five iterations, the first copy is guarded by cond, the second by another cond, and so on; this is trace-equivalent for paths with at most five iterations (Section 5.3, pp. 25â26). Automatic bound detection handles simple finite loops, but data-dependent or unbounded loops require --unwind B or per-loop --unwindset L:B.
The exact steps are:
- inspect loop identifiers with
--show-loop-ids; - choose a global
--unwind Bor per-loop bound; - use
--unwinding-assertionsso CBMC asserts that each bound is sufficient; - treat a successful run without complete unwinding as bounded evidence only.
The paper warns that --partial-loops changes the program: it inserts neither assumptions nor assertions, and can produce traces impossible in the original (Section 5.3, p. 24). It also notes that unwinding counts backjumps, so a loop testing i<100 needs a limit of 101 (Section 5.3, p. 26).
Inference: Verusâs loop invariants are the unbounded analogue. A Verus proof should state and prove an invariant rather than silently selecting an execution bound; bounded harnesses can still use CBMC-style finite exploration to find bugs before invariant proof.
technique: nondeterministic inputs and assumptions
Use nondeterministic functions to model arbitrary inputs, then constrain them with __CPROVER_assume. The paperâs one_to_hundred example calls nondet_uint, assumes result>=1 && result<=100, and returns the result (Section 3.3, pp. 12â13). Assumptions must be satisfiable or verification passes vacuously. They are not retroactive: an assertion before the assumption is unaffected. Nondeterminism also does not implement universal quantification; assuming a[x]>=0 for one nondeterministic x does not prove a[y]>=0 for another (Section 3.3, p. 13).
Inference: Verus uses universally quantified specifications and explicit requires predicates, so an assumption should become a precondition only when it describes the actual caller contract. A ghost nondeterministic input can model an arbitrary value, but adding a satisfiability check or concrete harness is necessary to avoid vacuous proofs.
technique: counterexample-guided debugging
When SAT finds a model, CBMC decodes guarded SSA assignments into a source-level trace. For a login buffer, --bounds-check --pointer-check --trace reports the out-of-bounds index and the repeated getchar values that trigger it (Section 3.2, pp. 10â11). This makes a Boolean assignment actionable: reproduce the path, add a guard or fix the code, then rerun.
Inference: VL can use counterexample traces as a repair input for agent-written Rust, but must distinguish a bounded witness from a proof failure. The trace should be stored with property name, source location, model, architecture, and unwind bound.
architecture and evidence
The intermediate GOTO language represents assignments, guarded gotos, calls, declarations, assertions, and assumptions. SSA conversion renames assignments and combines guards into equations (Section 5.2â5.3, pp. 20â26). CNF encoding is bit-precise; SMT back ends include Z3 and CVC4 and are selected with --smt2, --z3, or --cvc4 (Section 5.4, p. 26). âThe CNF formula, which can be printed using âdimacs, is passed to the SAT solver, which tries to find a satisfying assignment.â (Section 5.4, p. 26) The trusted components include the C semantics and front end, encoding, selected solver, and CBMCâs trace decoder; the paper does not claim unbounded completeness and lists âlimited support for unbounded verificationâ as a weakness (Table 1, p. 2).
The paperâs numerical examples have explicit settings: the overflow run uses CBMC 5.12 on 64-bit x86_64 macOS and reports 221 SAT variables, 203 clauses, and 0.00414769 seconds (Listing 3, p. 4). These are one run, not a benchmark denominator. No aggregate accuracy percentage is claimed in the tutorial chapter. CBMCâs practical strength is therefore the soundness of each bounded result and useful counterexample, while completeness depends on bounds or additional reasoning.
Inference: Kaniâs CBMC backend makes these controls directly relevant to VL. Agent-written verified Rust should record architecture, integer widths, loop bounds, assumptions, and solver mode; otherwise a green bounded result is difficult to interpret or reproduce.
what VL should reuse and what it should not redo
Reuse CBMCâs explicit property instrumentation, SSA-style trace decoding, assumption-vacuity warnings, loop-bound assertions, and machine-readable output options (--json-ui/--xml-ui, Section 5.1, p. 20). Do not redo a bounded checker without preserving the distinction between a finite safety result and an unbounded proof; the chapter makes that limitation central.
techniques for the catalog
- bit-precise bounded model checking: unroll finite executions and solve assertion violations as SAT/SMT constraints
- property instrumentation: combine user assertions with generated overflow, memory, and bounds checks
- loop unwinding assertions: detect insufficient bounds instead of silently assuming loop exit
- nondeterministic assumptions: model arbitrary inputs while guarding against vacuous constraints
- SSA counterexample traces: decode solver models into source-level failing executions Checked 2026-10-14 against the paper by a Codex agent: audit record kroening2023cbmc; compared the full paper and study, corrected metadata where needed, and did not rerun the experiments
Last edited: