tools-spin-model
Create a new Spin/Promela formal verification model for a system property
- License 许可
- Apache-2.0
- 最近更新
- 2026-11-12
- Stars
- ★ 3
- Forks
- 4
01 前言 Front Matter
- name
- tools-spin-model
- description
- Create a new Spin/Promela formal verification model for a system property
- argument-hint
- [property-number or description of property to prove]
03 SKILL.md 正文
Create 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. ⚠️ The "unless … eviction intervened" clause deliberately tolerates an eviction-caused miss — this is the exact loophole through which the Check→Pin race shipped. If the client observes the key before loading it, that tolerance is unsafe: use #12 instead (or in addition), which forbids removing a key any live client has observed.
- 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.
- Observe→use resolvability under eviction (Check→Pin) — Once a client (e.g. a vLLM connector) has observed a key resident in one RPC without pinning it, a later pinned load of that key in a separate RPC never observes NotExist. Concurrent eviction may demote the key (still resolvable) but must never remove it out from under the observing client. This is strictly stronger than #4: #4 permits an eviction-drop as a legitimate departure, whereas this property forbids removing a key any live client has observed. (This is the class of bug fixed by
evolve-dispatcher-dw; seemodelling/spin/check-pin-eviction-race/.)
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-12), 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 - 12 →
check-pin-eviction-race - Custom → ask user for a kebab-case name
- 1 →
-
Read BOTH the specification and the source code. The spec tells you which states are allowed (the intended contract); the code tells you which states are reachable (the real protocol). A model built from code alone encodes what the code does, not what it should do — so it will faithfully reproduce a bug and pass. Derive the safety assertion from the spec; derive the process structure and reachable transitions from the code.
Read the spec first (this is Spec-Kit driven — every component has
components/<c>/specs/<NNN-feature>/):- Dispatcher (properties 1–4, 9, 12):
components/dispatcher/specs/001-dispatcher-cache-interface/—spec.md(functional requirements, FR-IDs),data-model.md(entity states & transitions),contracts/idispatcher.md(per-operation pre/post-conditions),contracts/errors.md(which error each operation may legally return — the authority for "may a lookup return NotExist after populate?"),checklists/requirements.md. Alsoinfo/DESIGN.mdand theknowledge/wiki (e.g.size-mismatch-handling.md). - Extent-manager (properties 6, 11):
components/extent-manager/specs/001-extent-manager-v2/spec.md(+ anycontracts/,data-model.md). - Block-device (properties 5, 10):
components/block-device-spdk-nvme/specs/001-spdk-nvme-block-device/spec.md,data-model.md,contracts/iblock_device.md.
For the chosen property: locate the requirement / contract clause that states the guarantee (cite its FR-/requirement ID), and phrase the model's
assert()as the negation of a spec-permitted-outcome violation — not as your paraphrase of the code. If the spec is silent on the property (e.g. it does not say whether eviction may render an observed key unresolvable), that gap is itself a finding: note it, model the stronger safe interpretation, and flag it for the user / spec owner.Then read the source code to understand the real synchronization protocol and build the reachable state space. 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) - 12:
components/dispatcher/src/lib.rs(evict_one_cleandemote-vs-remove,batch_lookupclassify→load,dm.lookup/dm.remove/convert_to_storage), plus the connector's Check RPC vs Load RPC split. Reference model:modelling/spin/check-pin-eviction-race/check_pin_eviction_race.pml.
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. - Dispatcher (properties 1–4, 9, 12):
-
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
Bug-finding conventions (whole system) — these are what make a model able to catch a race, not just pass:
- Model observe-then-act client flows as SEPARATE steps. When a client learns something in one RPC and acts on it in a later RPC (e.g. a connector Checks a key resident, then Pins+Loads it in a separate call; or a store that prepares then commits), model the two as distinct steps with the intervening window left open to all other processes. Collapsing them into one atomic pin-on-hit hides exactly the races that live in the gap. Only the use step takes the read-ref/pin; the observe step must not pin. (This gap is where the Check→Pin eviction race lived.)
- Distinguish eviction/removal OUTCOMES, not just "evicted." A demote (
convert_to_storage: entry flips to BlockDevice, still resolvable via cold-path promote) and a full remove (dm.remove: entry becomes NotExist) have completely different consequences for a client. Model them as separate transitions. Treat removing a key that any live client has observed but not yet pinned as a candidate hazard, and assert against it. - Inject the failure/error branches that make hazardous states reachable. Many bugs are only reachable after a fallible op fails: a write-through that fails (
IoError) releases the writer's pin while leaving the entry unpersisted; adm.lookupthat times out; an allocation that fails. Add nondeterministic failure branches (if :: ok :: fail fi) for every fallible cross-component op the property depends on. Without them the hazardous state (e.g. unpinned-AND-unpersisted) is unreachable and the model is falsely green. - Classify each terminal client outcome as FATAL or GRACEFUL, and assert no reachable path is fatal. Map the modeled outcome back to the real consequence: a load miss forwarded to remote-lookup is fatal (IoError →
EngineDeadError→ vLLM crash); anAllocationFailedthat serves uncached is graceful. The safety assertion should forbid reaching a fatal terminal, not merely "some error." (A property that lumps fatal and graceful misses together as "KeyNotFound is allowed" is too weak — that is exactly why property #4 tolerated this bug.)
-
Add a known-bad MUTANT and prove the model rejects it (MANDATORY).
A model that only ever passes on the current code proves nothing about its power to catch a bug — it may simply be too weak. For every safety property, gate the suspected-hazardous behavior behind a Promela compile-time switch and prove the model fails on the mutant:
- Pick the hazard the property guards against and express its buggy variant behind
#ifdef INJECT_<HAZARD>(e.g.#ifdef BUGGY_DROP_FALLBACKtoggles adm.removeof an unpinned unpersisted victim vs. skipping it). Keep the two branches minimal and directly mirror a real code path that either exists in history or is a plausible regression. - The fixed build (switch off) must verify with 0 errors; the mutant build (switch on) must produce an assertion violation. If the mutant still passes, the property is too weak — strengthen the assertion (usually: make it forbid a fatal outcome, or assert on a finer-grained state) until the mutant fails, then confirm the fixed build still passes.
- Wrap the buggy-only
#ifdefarms so that in the fixed build they are pruned, not left as dead unreached states. A branch that is deliberately unreachable in the fixed build (e.g. the fatal-miss arm) is expected and is itself evidence that the fatal path is dead — call it out in the README rather than forcing it reachable. - Gotcha:
#define/#ifdefin Promela are processed byspin -a(which runs the model through the C preprocessor), NOT bycc. Pass the switch asspin -DINJECT_<HAZARD> -a model.pml. The generatedpan.cdiffers between builds, so each build must regenerate it. To replay a mutant trail you must pass the same-Dtospin -t.
For a worked example see
modelling/spin/check-pin-eviction-race/(make= fixed, 0 errors;make buggy= mutant, assertion violation + trail). - Pick the hazard the property guards against and express its buggy variant behind
-
Write the Makefile
modelling/spin/<name>/Makefile(includes the mandatorymutanttarget — renameINJECT_HAZARDto your switch):MODEL = <name_underscored>.pml CC = cc CFLAGS = -O2 DEPTH = 200000 MUTANT = INJECT_HAZARD .PHONY: all safety mutant liveness clean # Default: verify the FIXED system. Expected: 0 errors, 0 unreached states. all: safety safety: spin -a $(MODEL) $(CC) $(CFLAGS) -DSAFETY -o pan pan.c ./pan -m$(DEPTH) # Known-bad mutant. Expected: an assertion violation + a .trail. # The -D is a Promela (cpp) switch, so it goes to `spin -a`, not to cc. mutant: spin -D$(MUTANT) -a $(MODEL) $(CC) $(CFLAGS) -DSAFETY -o pan-mutant pan.c ./pan-mutant -m$(DEPTH) liveness: spin -a $(MODEL) $(CC) $(CFLAGS) -o pan-live pan.c ./pan-live -a -m$(DEPTH) clean: rm -f pan pan-mutant pan-live pan.* *.trail -
Write 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)
- Specification basis section: for each asserted property, cite the spec clause it enforces (
spec.mdFR-ID,contracts/errors.md, or the requirements checklist), and note any spec gap the model had to resolve by choosing the stronger safe interpretation - Mutant section: name the known-bad switch, what it toggles, and the expected fixed-vs-mutant outcomes
- Running section with shell commands (including
make mutantand the mutant trail replay) - Tuning the Model section explaining parameters
- Correspondence table with THREE columns: Model location → Intended behavior (spec §/FR-ID) → Implemented behavior (source file:line). Where the spec and code columns diverge, that row is a finding.
- Title:
-
Run BOTH verifications in the new directory:
make(fixed) andmake mutant.- If Spin is not installed, inform the user and skip this step.
- The fixed build (
make) must report 0 errors. The mutant build (make mutant) must report ≥1 assertion violation; replay its trail withspin -t -p -g -l -D<MUTANT> <file>.pmland confirm the interleaving is the intended hazard. - If the fixed build fails, first decide whether it is a real defect in the code (analyze the trail, report it to the user — do not silently weaken the model to make it green) or a modeling artifact (over-abstraction, a missing pin, wrong atomicity). Only relax the model when it is genuinely an artifact; a real counterexample against current code is a finding, not a bug in the model.
- If the mutant build passes, the property is too weak — strengthen it (per the bug-finding conventions above) until the mutant fails, then re-confirm the fixed build still passes.
- If the fixed build passes but contradicts the spec — i.e. the code is self-consistent yet the modeled behavior violates a
spec.md/contracts/clause — that is a code-vs-spec divergence. Report it as a finding (the model verified the wrong contract); do not treat a green run as success when the assertion came from the code instead of the spec.
-
Report results to the user:
- Scope (single component or whole system)
- Property being verified, and the spec clause (FR-ID / contract) it was derived from — plus any spec gap or code-vs-spec divergence surfaced
- Fixed build: pass/fail status, state space size, depth, coverage (target: 0 unreached in all proctypes, modulo deliberately-pruned mutant-only branches)
- Mutant: which hazard it injects, and confirmation the model catches it (violation + trail)
- Path to the new model directory
Notes
- Assert from the spec, not from the code. The property you check must come from the specification (
spec.md/contracts// requirements checklist), because a model whose assertion is paraphrased from the implementation can only ever confirm the code agrees with itself — it cannot catch a bug the code and your reading of it share. The code is for the reachable states; the spec is for the allowed states. (See step 2.) - A model that only passes proves nothing. The next most important discipline (step 5) is the known-bad mutant: a property is only as good as its ability to reject a buggy variant. If you cannot construct a plausible mutant that the model catches, the property is probably too weak to catch a real regression either. This is precisely why property #4 (populate-lookup linearizability) passed while the Check→Pin race shipped — it was written to tolerate the eviction-drop, and no mutant ever forced it to confront one.
- Spin is installed at
~/.local/bin/spinon this machine (runmodelling/spin/install-spin.sh --prefix $HOME/.localand put$HOME/.local/binonPATH; the older/usr/local/bin/spinlocation also works if present). The optional Tcl/Tk GUI isispin(needs thetkpackage forwish). - 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.
04 同仓库更多 Skills
tools-spin-sync
Synchronize Spin/Promela models with current source code and re-verify
tools-spin-model
Create a new Spin/Promela formal verification model for a system property
tools-creusot-install
Install the Creusot Rust verification tool
tools-creusot-init
Prepare an existing Cargo project to use Creusot formal verification
tools-creusot-coq-install
Add Coq 8.20 as an additional prover to an existing Creusot installation, for verification conditions that SMT solvers cannot discharge
tools-creusot-annotate-interfaces
Formally verify a component's properties with Creusot and annotate its interface definitions