tools-spin-model
Create a new Spin/Promela formal verification model for a system property
SKILL.md.claude/skills/tools-spin-model/SKILL.md
GitHubCreate a new Spin/Promela specification in modelling/spin/<name>/ to verify a concurrency property of the Certus system.
Input
If no argument is provided, first ask the user to define the scope, then present the property menu.
Step A: Scope Selection
Ask the user to choose the modeling scope:
- Single component — Model the internal concurrency of one component in isolation (e.g., just the dispatcher, just the extent-manager, just the block-device actor). Threads and channels within that component are modeled explicitly; interactions with other components are abstracted as nondeterministic environment stubs.
- Whole system — Model the interaction between multiple components as wired together in certus-server (e.g., client → dispatcher → memory-tier → block-device → extent-manager). Includes cross-component communication channels, the gRPC thread pool, and background workers.
The scope determines:
- Which source files to read (single component = one crate's
src/; whole system = multiple crates +apps/certus-server/) - How to abstract boundaries (single = environment stubs with nondeterministic responses; whole = explicit
proctypeper component role) - Model complexity (single = fewer processes, deeper per-component detail; whole = more processes, coarser per-component abstraction)
Step B: Property Selection
Present the full property menu and ask the user to choose. The user may also describe a custom property.
System-level properties (scope: whole system recommended):
- Reference-count balance — Every read_ref increment has a matching decrement on all paths. No leaked refs (memory leak) or double-decrements (use-after-free).
- Write-before-evict — Models the dispatcher's populate → write-through → eviction lifecycle to verify that memory-tier eviction never produces dangling SSD references.
- Cold-path promotion atomicity — At most one thread succeeds in promoting a given key from SSD to memory-tier. No double-allocation or lost updates.
- Populate-lookup linearizability — After populate(key) returns Ok, a concurrent lookup(key) never observes KeyNotFound unless an explicit remove or eviction intervened.
- Shutdown ordering — No NVMe command is issued after its block device shuts down. Background writer drains before drives are torn down.
- No lost extents — Every reserve_extent is followed by either publish() or drop (abort). No extent is permanently allocated but uncommitted.
Component-level properties (scope: single component recommended):
- Pipeline ring slot safety — A ring buffer slot is never resubmitted for NVMe read while its CUDA DMA transfer is still in flight. (Component: dispatcher/pipeline)
- Starvation freedom — No client is starved indefinitely waiting to enqueue a write job under backpressure. (Component: dispatcher/background)
- Eviction fairness — An entry touched within the last N operations is never evicted while untouched entries exist. (Component: dispatcher/eviction)
- Actor mailbox ordering — Messages sent to a block-device actor are processed in FIFO order; no command reordering across the channel boundary. (Component: block-device-spdk-nvme)
- Extent bitmap consistency — Concurrent reserve/publish/remove never leave the bitmap in a state where an extent is both allocated and free. (Component: extent-manager)
If the user provides a number (1-11), use that property. If they provide free text, treat it as a custom property description and ask for a short kebab-case name for the subdirectory.
Steps
Determine the property and directory name.
Map selections to directory names:
- 1 →
ref-count-balance - 2 →
write-before-evict - 3 →
promotion-atomicity - 4 →
populate-lookup-linearizability - 5 →
shutdown-ordering - 6 →
no-lost-extents - 7 →
pipeline-ring-safety - 8 →
starvation-freedom - 9 →
eviction-fairness - 10 →
actor-mailbox-ordering - 11 →
extent-bitmap-consistency - Custom → ask user for a kebab-case name
- 1 →
Read the relevant source code to understand the real synchronization protocol.
Key source files by property and scope:
Whole-system scope (read certus-server wiring + component internals):
- 1,4:
components/dispatcher/src/lib.rs(populate, lookup, evict_for_space, batch_lookup) - 2:
components/dispatcher/src/lib.rs+components/dispatcher/src/background.rs - 3:
components/dispatcher/src/lib.rs(batch_lookup cold-path promotion, lines ~1045-1215) - 5:
components/dispatcher/src/lib.rs(shutdown),components/dispatcher/src/background.rs(BackgroundWriter::shutdown),apps/certus-server/src/main.rs(shutdown sequence) - 6:
components/dispatcher/src/lib.rs(prepare_store, commit_store, cancel_store, process_write_job)
Single-component scope (read only the target component):
- 7:
components/dispatcher/src/pipeline.rs(PipelineRing, pipelined_ssd_to_gpu_zero_copy) - 8:
components/dispatcher/src/background.rs(channel + worker_loop) - 9:
components/dispatcher/src/lib.rs(evict_for_space, mt.evict_lru, mt.oldest_keys) - 10:
components/block-device-spdk-nvme/src/(actor thread, command channel, completion channel) - 11:
components/extent-manager/src/(bitmap allocation, reserve_extent, publish, remove_extent)
Also reference the existing model at
modelling/spin/write-before-evict/write_before_evict.pmlfor style and conventions.- 1,4:
Create the subdirectory
modelling/spin/<name>/.Write the Promela specification
modelling/spin/<name>/<name_underscored>.pml.Follow these conventions (from the existing write-before-evict model):
- Header comment block explaining what is being verified, the scope, and how to run
#defineparameters section (N_CLIENTS, N_KEYS, etc.) tuned small for tractable state space- Per-key or per-resource state as
mtypeenums and arrays proctypefor each thread role in the systeminlinehelpers for shared logic (like eviction)- Inline
assert()statements for safety properties initprocess that starts all proctypes, waits for completion, signals shutdown, runs final invariant checks- Aim for 100% state coverage (no unreached states)
- Use
atomic{}for operations that share a mutex in the real code - Use
d_step{}for deterministic sequences (scans) - Use generation counters to handle key reuse across lifecycles
- Model channels as bounded
chan(even if real code uses unbounded)
Scope-specific conventions:
For single component models:
- Abstract external interfaces as nondeterministic stubs (e.g.,
if :: success :: failure fi) - Focus on internal thread interactions and shared state
- Name the header comment "Component-level model: <component-name>"
- Document which interfaces are stubbed and what assumptions are made
For whole system models:
- One
proctypeper major thread role across components - Use channels to model inter-component communication (gRPC, receptacles)
- Name the header comment "System-level model: <property-name>"
- Document the component wiring in the header
Write the Makefile
modelling/spin/<name>/Makefile:MODEL = <name_underscored>.pml PAN = pan CC = cc CFLAGS = -O2 DEPTH = 200000 .PHONY: all safety liveness clean all: safety safety: $(PAN) ./$(PAN) -m$(DEPTH) liveness: $(PAN)-live ./$(PAN)-live -a -m$(DEPTH) $(PAN): pan.c $(CC) $(CFLAGS) -DSAFETY -o $@ $< $(PAN)-live: pan.c $(CC) $(CFLAGS) -o $@ $< pan.c: $(MODEL) spin -a $< clean: rm -f $(PAN) $(PAN)-live pan.* *.trailWrite the README.md
modelling/spin/<name>/README.md:Structure (follow the pattern in
modelling/spin/write-before-evict/README.md):- Title:
# <Property Name> - Scope: Single component (
<component-name>) or Whole system - One-paragraph description of what the model verifies
- Properties Verified table (ID, Property description, Type: Safety/Liveness)
- System Abstraction table (Real component → Promela process)
- Assumptions / Stubs section (for single-component scope: list what is abstracted away and how)
- Running section with shell commands
- Tuning the Model section explaining parameters
- Correspondence to Source Code table (Model location → Source file → Line range)
- Title:
Run the verification by executing
makein the new directory.- If Spin is not installed, inform the user and skip this step.
- If verification fails with an assertion violation, analyze the trail (
spin -t -p <file>.pml), fix the model, and re-run until it passes. - Report: number of states explored, depth reached, errors found, coverage (unreached states).
Report results to the user:
- Scope (single component or whole system)
- Property being verified
- Pass/fail status
- State space size
- Coverage (target: 0 unreached states in all proctypes)
- Path to the new model directory
Notes
- Spin must be installed at
/usr/local/bin/spin(seemodelling/spin/write-before-evict/README.mdfor install instructions). - Keep parameters small (2-3 clients, 2-4 keys, pool cap < key count) to maintain tractable state spaces (<10M states).
- The model should be self-contained — no external dependencies beyond Spin and a C compiler.
- Use the generation-counter technique from the write-before-evict model to handle key reuse without conflating lifecycles.
- Single-component models are faster to verify (fewer processes) and better for finding internal races.
- Whole-system models catch cross-component protocol violations but have larger state spaces.