The model file
A declarative file that points libcmc at the binary you ship, the environment to explore, and the invariants to check.
---A model file tells libcmc what to check without reimplementing it. It names the binary to run, how many nodes, how to read a node’s state, the environment budget to explore, and the invariants. It carries no behaviour: the protocol is the running binary, not anything written here.
Format
INI-style sections of key = value. Comments start with #. Three
sections: [target], [environment], [invariants].
[target]
binary = ./replog # the program to check, run under interception
nodes = 3 # number of node processes
state = canon # how to read a node's state (see below)
[environment]
clients = 1 # client requests per node (0-255)
drops = 1 # messages the network may lose
crashes = 1 # node crashes (partial-durability branching)
dups = 0 # duplications (needs net = bag)
net = fifo # fifo (default) or bag
net_cap = 8 # messages in flight at once
persist_cap = 2 # pending disk writes per node
reply_cap = 8 # client replies recorded
live_steps = 1000 # bound for eventually-properties
[invariants]
# name = expression (see "Invariant language")
no_lost_ack = "all r in replies: r.val in node[r.src].durable"
agreement = "all i,j in nodes: committed[i]==0 or committed[j]==0 or committed[i]==committed[j]"
[target].state
How the checker reads one node’s state for dedup and for invariants:
canon– the node emits a canonical encoding of its logical state on file descriptor 3 after each step, as a self-delimiting record. This is the recommended form; the encoding is order-independent, so two interleavings that reach the same logical state dedup correctly. See “The state hook”.region:SYMBOL– the checker reads a fixed byte region named by an ELF symbol (the target must be-no-pie). Simpler, but the raw bytes must already be canonical (no order-dependent layout, no uninitialised padding), so it suits only flat, pointer-free state.
The state hook
With state = canon, the target exports its logical state like this
(about eight lines, no protocol logic):
/* called after each handled event; emits [count][bytes...] on fd 3 */
static void cmc_emit(void)
{
unsigned char buf[256];
size_t n = serialize_logical_state(buf + 1, sizeof buf - 1); /* your encoding */
buf[0] = (unsigned char) n;
(void) !write(3, buf, n + 1);
}
The encoding must be canonical (the same logical state always produces the same bytes: sort sets, omit addresses) and complete (every byte that affects future behaviour is included; anything omitted can let the checker merge two states that differ and miss a bug). Completeness is the one property the operator is trusted to get right; treat it like the durable/volatile split in a protocol core.
Invariant language
A small expression language, evaluated over each node’s canonical state and the ghost reply log. It is deliberately not Turing-complete: no loops beyond the quantifiers, no function definitions.
Values: integers, byte arrays (the canonical state), and the reply log.
node[i]– nodei’s canonical state (a byte array),0 <= i < nodes.node[i].NAME– a named field, if the model file declares a layout (future; for now the state is addressed by byte offsetnode[i][k]).replies– the ghost log; each entry has.src(node) and.val.committed[i],durable[i]– named views the model file maps onto the canonical bytes (future; initially byte accessors).
Quantifiers and operators:
all V in SET : EXPR # holds for every binding
some V in SET : EXPR # holds for at least one
EXPR and EXPR EXPR or EXPR not EXPR
A == B A != B A < B A <= B A in B
An invariant holds in a state when its expression is true there. A violation prints the shortest sequence of environment actions that reaches the failing state, and writes a replay file.
C-predicate escape hatch
For a property the DSL cannot express, the target may export
int cmc_check_NAME(void) returning non-zero on violation, and the model
file references it:
[invariants]
hard_property = c:cmc_check_hard_property
The predicate runs in the node process over its real state. It is a small amount of code that can carry a bug, so prefer the DSL; the hatch exists so a hard property never forces a protocol rewrite.
Running
cmc --model-file replog.model # exhaustive BFS
cmc --model-file replog.model --simulate --seed 7 --walks 100000
cmc --model-file replog.model --trace-out bug.replay
cmc --replay bug.replay
Exit status is the engine’s: 0 pass, 1 violation, 2 incomplete (a limit stopped the search), 3 a usage or model-file error.
Status
The schema is fixed here; the implementation lands incrementally
(experiments/process-mode/, then src/). The first milestone is a
single [target] node with state = canon, [environment] budgets, and
byte-offset invariants; named fields, multi-node and the C escape hatch
follow.