Model Checking
Writing a
temporal-logic
specification is only half the battle. Now comes the question that has occupied verification for forty
years: given a model of the system M and a temporal formula
\varphi, does the system satisfy the specification? We
write this M \models \varphi. What makes model checking
revolutionary is that, for finite-state systems, this question is decidable and automatic:
you hand a tool the model and the property, and it answers "yes" — or, far more usefully, hands you a
concrete counterexample showing exactly how the property fails.
That last feature is the killer app. A theorem prover that says "your invariant does not follow" leaves
you to guess why. A model checker that says "no" gives you a precise, replayable execution trace
marching into the bug. This is push-button verification, and it earned Edmund Clarke, E. Allen Emerson,
and Joseph Sifakis the 2007 Turing Award.
The model: a Kripke structure
The system is modelled as a Kripke structure (equivalently, a finite transition
system): a finite set of states, a transition relation saying which state can follow which, an initial
state, and — crucially — a labelling that tags each state with the atomic
propositions true there. Formally M = (S, S_0, R, L) with
R \subseteq S \times S total (every state has a successor, so runs are
infinite) and L : S \to 2^{AP}.
The paths through this graph are the behaviours: unrolling the Kripke structure from the
initial state gives exactly the computation tree the temporal logic talks about. So
M \models \varphi means "every path (or, for CTL, the branching structure)
from S_0 satisfies \varphi" — a question about a
finite graph, which is why it can be decided by search.
CTL model checking: iterative fixpoint labelling
CTL has an elegant algorithm that is a joy to run by hand. Work bottom-up over the syntax
tree of \varphi: for each subformula, compute the exact set of
states that satisfy it, labelling states as you go. Atomic propositions are read straight off
L; boolean connectives are set operations on the labels; the temporal
modalities are computed by fixpoint iteration over the transition relation.
- \mathbf{EX}\,\varphi: label every state that has a successor
already labelled \varphi — one backward step along
R.
- \mathbf{EF}\,\varphi (reachability): start from the
\varphi-states and repeatedly add any state with a successor already in
the set, until nothing changes — a least fixpoint.
- \mathbf{EG}\,\varphi: start from all
\varphi-states and repeatedly remove any that has no successor
still in the set, until nothing changes — a greatest fixpoint (the states that can stay
in \varphi forever).
Every CTL modality reduces to \mathbf{EX}, \mathbf{EU}, \mathbf{EG} plus
negation, so these three iterations decide any CTL formula. Each iteration touches every state and
edge at most a bounded number of times, giving the celebrated bound: CTL model checking runs in
O(|\varphi| \cdot (|S| + |R|)) — linear in both the
formula and the model.
LTL model checking: the automata-theoretic product
LTL takes a different, equally beautiful route — the automata-theoretic approach of
Vardi and Wolper. To check M \models \varphi, ask the complementary
question: is there any run of M that violates
\varphi, i.e. that satisfies \neg\varphi?
- Negate and translate. Build a Büchi automaton
A_{\neg\varphi} that accepts exactly the infinite traces violating
\varphi. (A Büchi automaton accepts an infinite word when a run visits
an accepting state infinitely often.)
- Take the product. Form M \times A_{\neg\varphi},
synchronising the system's runs with the automaton's guesses. Its accepting runs are exactly the
behaviours of M that break \varphi.
- Check emptiness. Search the product for a reachable accepting
cycle — a lasso whose loop passes through an accepting state. Found one? That lasso is a
counterexample. None exists? Then M \models \varphi.
The "reachable accepting cycle" search is done by a nested depth-first search over
the product graph. The whole procedure is linear in the size of the product but the automaton
A_{\neg\varphi} can be exponential in |\varphi|,
so LTL model checking is O\!\left(|S| \cdot 2^{O(|\varphi|)}\right) —
cheap in the (usually small) formula, linear in the (usually huge) state space. Note the shapes match
the previous lesson: a safety violation surfaces as a finite path to a bad state, a liveness violation
as an accepting cycle.
Counterexamples: the feature that changed engineering
Why did model checking, rather than theorem proving, break into industry? Because it is
refutation-oriented. When the property fails, the tool does not shrug — it produces the
witness the theory guarantees: a finite trace for a safety bug, a lasso for a liveness bug, replayable
step by step in the very states of your model. Engineers debug against it exactly as against a failing
test, but this "test" was found by exhaustively searching all behaviours, not the handful you
thought to write.
The industrial track record is why this matters. Hardware model checking is standard practice at every
major chip maker — Intel adopted it in earnest after the 1994 Pentium FDIV bug. Symbolic model
checkers found real errors in the cache-coherence protocols of the IEEE Futurebus+ standard and in
published network and security protocols. NASA and aerospace groups model-check flight-control and
mission software. The pattern is always the same: an exhaustive search over an astronomically large
state space, and a short, damning counterexample when something is wrong.
Verifying arbitrary programs is undecidable — that is the
halting
problem talking. Model checking escapes not by magic but by restriction: it
decides M \models \varphi for a finite-state
M. With finitely many states, "runs forever" just means "revisits a state",
which a graph search can detect — the infinite behaviours collapse into finite lassos. The catch is
that reducing a real, unbounded program to a finite model requires abstraction, and the finite model
can still be enormous. That enormity is the price of decidability, and it has a name:
the
state-explosion problem.
A model checker returning "yes" is a proof about M, the abstraction — not
automatically about the real system. If your Kripke structure omits a behaviour the real hardware or
code can exhibit (a missing transition, an over-coarse abstraction, a wrong assumption about the
environment), the tool can honestly certify M \models \varphi while the
deployed artifact still fails. The verdict is only as trustworthy as the model and the property. So the
real discipline is twofold: model faithfully, and specify what you actually mean — a "yes" against a
sloppy model or a too-weak formula is false comfort. (Counterexamples, by contrast, are usually more
robust: a concrete failing run is a strong hint of a genuine bug, or at least of a modelling mistake
worth understanding.)