SPIN and NuSMV: Model Checkers for Concurrent System Verification
Race conditions are one of the hardest bugs to find with tests. A threading bug might only trigger after a specific interleaving of 8 operations across 3 goroutines, and the window for that interleaving might be 10 nanoseconds. You could run the test a million times and never hit it — until production.
Model checkers for concurrent systems take a different approach: instead of running the program, they exhaustively explore all possible execution orderings. SPIN and NuSMV are two mature, widely-used model checkers that can find concurrency bugs, protocol violations, and state machine errors that would take months of testing to stumble upon.
SPIN: Concurrent Systems Model Checking
SPIN (Simple Promela Interpreter) is the most widely used explicit-state model checker. It verifies models written in Promela (Process Meta Language), an executable specification language for concurrent systems.
SPIN was developed at Bell Labs, won the ACM Software System Award in 2001, and has been used to verify protocols in NASA space missions, network protocols, and concurrent algorithms.
Installing SPIN
# macOS
brew install spin
# Ubuntu
apt install spin
# Or build from source
wget https://spinroot.com/spin/Src/spin649.tar.gz
tar xzf spin649.tar.gz
cd Spin/
make
sudo mv spin /usr/local/bin/Promela: Concurrent Processes
Promela models concurrent processes communicating via channels:
/* Simple producer-consumer with a buffer of size 1 */
mtype = { ITEM };
chan buffer = [1] of { mtype };
bool produced = false;
bool consumed = false;
active proctype Producer() {
buffer ! ITEM;
produced = true;
}
active proctype Consumer() {
mtype item;
buffer ? item;
consumed = true;
}
/* LTL property: eventually both produced and consumed */
ltl correctness { <> (produced && consumed) }Run SPIN:
# Verify LTL property
spin -a model.pml
gcc -o pan pan.c
./pan -a # check all acceptance cycles
# Simulate (random execution)
spin -p -u 100 model.pmlModeling a Mutex Protocol
/* Peterson's mutual exclusion algorithm */
bool flag[2] = false;
int turn = 0;
bool critical[2] = false;
/* Safety: at most one in critical section */
#define mutex (!(critical[0] && critical[1]))
active [2] proctype Process()
{
int i = _pid; /* 0 or 1 */
int j = 1 - i; /* the other process */
do
:: /* Entry protocol */
flag[i] = true;
turn = j;
/* Wait until it's safe to enter */
(flag[j] == false || turn == i);
/* Critical section */
critical[i] = true;
assert(mutex); /* safety check */
critical[i] = false;
/* Exit protocol */
flag[i] = false;
od;
}
ltl mutual_exclusion { [] mutex }
ltl no_starvation {
[] (flag[0] -> <> critical[0]) &&
[] (flag[1] -> <> critical[1])
}SPIN verifies mutual_exclusion — that both processes are never in the critical section simultaneously — by exploring every possible interleaving.
LTL Properties in SPIN
Linear Temporal Logic (LTL) operators in SPIN:
| Operator | Meaning | Promela Syntax |
|---|---|---|
[] P |
Always P | [] P |
<> P |
Eventually P | <> P |
P U Q |
P until Q | P U Q |
P -> Q |
P implies Q | P -> Q |
Common patterns:
/* Safety: bad state never reached */
ltl safety { [] !bad_state }
/* Liveness: request always eventually granted */
ltl liveness { [] (request -> <> granted) }
/* No starvation: process always eventually runs */
ltl no_starvation { [] <> running }
/* Bounded response: response within 3 steps */
ltl bounded { [] (req -> (granted || X (granted || X granted))) }Channel Communication and Deadlock Detection
/* Dining philosophers — check for deadlock */
#define N 5
chan fork[N] = [1] of { bool };
init {
int i;
for (i : 0 .. N-1) {
fork[i] ! true;
}
}
active [N] proctype Philosopher()
{
int left = _pid;
int right = (_pid + 1) % N;
bool f;
do
:: /* Pick up left fork */
fork[left] ? f;
/* Pick up right fork */
fork[right] ? f;
/* Eat */
/* Put down forks */
fork[right] ! true;
fork[left] ! true;
od;
}
/* SPIN will find the deadlock automatically — no ltl needed */
/* All philosophers hold left fork, wait for right */Run with deadlock detection:
./pan -d # detect deadlocksSPIN produces a counterexample trace showing the exact sequence of steps leading to deadlock.
Partial Order Reduction
For large models, SPIN's state space explodes. Partial order reduction (POR) prunes equivalent interleavings:
# Enable POR (default in most versions)
./pan -p # full POR
# BitState hashing for very large state spaces
./pan -DBITSTATE # compile with bitstateSupertrace (bitstate) sacrifices completeness for scalability — useful when you need to check large real-world protocols.
NuSMV: Symbolic Model Checking
NuSMV uses BDD-based (Binary Decision Diagram) symbolic model checking, which handles much larger state spaces than SPIN's explicit-state approach. It's especially effective for hardware designs and controllers.
Installing NuSMV
# macOS
brew install nusmv
# Ubuntu — usually needs manual install
wget https://nusmv.fbk.eu/NuSMV/download/getting-v2/NuSMV-2.6.0-linux64.tar.gz
tar xzf NuSMV-2.6.0-linux64.tar.gz
# Or use NuXMV (successor)
# https://nuxmv.fbk.eu/NuSMV Modules and State Machines
NuSMV models are state machines with explicit initial states and transition relations:
-- Traffic light controller
MODULE main
VAR
light : {red, yellow, green};
timer : 0..30;
ASSIGN
init(light) := red;
init(timer) := 0;
next(light) :=
case
light = red & timer >= 30 : green;
light = green & timer >= 25 : yellow;
light = yellow & timer >= 5 : red;
TRUE : light;
esac;
next(timer) :=
case
next(light) != light : 0;
TRUE : min(timer + 1, 30);
esac;
-- Safety: never directly green to red
LTLSPEC G (light = green -> X(light != red))
-- Liveness: green light eventually reached
LTLSPEC F (light = green)Run NuSMV:
NuSMV traffic.smvOutput:
-- specification G (light = green -> X light != red) is true
-- specification F (light = green) is trueCTL vs LTL in NuSMV
NuSMV supports both CTL (Computation Tree Logic) and LTL. CTL reasons about branching time:
| CTL Operator | Meaning |
|---|---|
EX P |
There exists a next state where P |
AX P |
All next states satisfy P |
EF P |
There exists a path where P eventually holds |
AF P |
All paths: P eventually holds |
EG P |
There exists a path where P always holds |
AG P |
All paths: P always holds |
E[P U Q] |
Some path: P until Q |
A[P U Q] |
All paths: P until Q |
-- CTL properties
CTLSPEC AG (light = red -> EF(light = green)) -- always possible to reach green from red
CTLSPEC AG !(light = red & light = green) -- never red AND green simultaneously
-- LTL properties
LTLSPEC G (light = red -> F light = green) -- always eventually green after red
LTLSPEC G (light = green -> !X(light = red)) -- no direct green->red transitionModeling a Protocol with NuSMV
Two-phase commit protocol:
MODULE coordinator(votes)
VAR
state : {INIT, WAIT, COMMIT, ABORT};
ASSIGN
init(state) := INIT;
next(state) :=
case
state = INIT : WAIT;
state = WAIT & votes = all_yes : COMMIT;
state = WAIT & votes != all_yes : ABORT;
TRUE : state;
esac;
MODULE participant(coordinator_decision)
VAR
state : {READY, COMMITTED, ABORTED};
ASSIGN
init(state) := READY;
next(state) :=
case
state = READY & coordinator_decision = COMMIT : COMMITTED;
state = READY & coordinator_decision = ABORT : ABORTED;
TRUE : state;
esac;
MODULE main
VAR
votes : {all_yes, some_no};
coord : coordinator(votes);
part1 : participant(coord.state);
part2 : participant(coord.state);
-- Agreement: both participants make the same decision
CTLSPEC AG (part1.state = COMMITTED -> part2.state = COMMITTED)
CTLSPEC AG (part1.state = ABORTED -> part2.state = ABORTED)
-- Termination: coordinator eventually decides
CTLSPEC AF (coord.state = COMMIT | coord.state = ABORT)Counterexample Analysis in NuSMV
When NuSMV finds a property violation, it produces a counterexample trace:
-- specification AG (...) is false
-- as demonstrated by the following execution sequence
Trace Description: CTL Counterexample
Trace Type: Counterexample
-> State: 1.1 <-
votes = all_yes
coord.state = INIT
part1.state = READY
-> State: 1.2 <-
coord.state = WAIT
-> State: 1.3 <-
votes = some_no <-- non-deterministic choice
coord.state = ABORT
-> State: 1.4 <-
part1.state = ABORTED
part2.state = COMMITTED <-- VIOLATION: inconsistent decisionsThe counterexample shows that when votes changes non-deterministically between the coordinator's decision states, participants can reach different conclusions.
CI Integration
# .github/workflows/model-check.yaml
name: Model Checking
on:
push:
paths:
- 'models/**/*.pml'
- 'models/**/*.smv'
jobs:
spin:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: apt-get install -y spin
- name: Check Promela models
run: |
for f in models/**/*.pml; do
spin -a "$f"
gcc -o pan pan.c
./pan -a || (echo "SPIN violation in $f"; exit 1)
done
nusmv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install NuSMV
run: |
wget -q https://nusmv.fbk.eu/NuSMV/download/getting-v2/NuSMV-2.6.0-linux64.tar.gz
tar xzf NuSMV-2.6.0-linux64.tar.gz
echo "$PWD/NuSMV-2.6.0-linux64/bin" >> $GITHUB_PATH
- name: Check SMV models
run: |
for f in models/**/*.smv; do
NuSMV "$f" | grep -E "^-- specification" | grep -v "is true" && exit 1 || true
doneChoosing Between SPIN and NuSMV
Use SPIN when:
- Verifying concurrent software protocols (Go channels, POSIX threads, message passing)
- You want to write executable specifications (Promela can be simulated)
- State space is small to medium (explicit state)
- You need LTL liveness checking
Use NuSMV when:
- Verifying hardware controllers or state machines
- State space is large (BDD symbolic checking handles it better)
- You want CTL branching-time properties
- You're verifying reactive systems
For distributed systems software, SPIN is usually the better fit. For embedded controllers and hardware models, NuSMV or its successor NuXMV.