component-poison-verif
Deliberately introduce a controlled number of concrete-code/abstract-code misalignments ("verif poisons") into a single Certus component on a throwaway branch, in order to test formal-verification dri
SKILL.md.claude/skills/component-poison-verif/SKILL.md
GitHubPoison a Component's Verification (concrete ↔ abstract)
The purpose of this skill is to test the formal-verification drift mechanisms by
deliberately introducing known, controlled mismatches between a component's concrete
(shipped) code and its abstract verification artifacts. Where component-poison attacks
the spec.md ↔ code link, this skill attacks the code ↔ verif link:
- Creusot — a
verif/mirror crate that copies a shipped function and proves a spec-derived contract with#[requires]/#[ensures], kept honest by a drift/equality check and fault injection (seetools-verify-creusot). - Kani — a
#[cfg(kani)] mod verificationwhose harnesses call the real function underkani::assumepreconditions that must mirror production guards (seetools-verify-kani). - Spin — a Promela
.pmlmodel undermodelling/spin/<name>/whose README maps model locations to source lines (seetools-spin-sync).
The verification skills then should detect these seeded mismatches — either by a verification condition (VC) going red / a counterexample, or (the harder, more important case) by the drift/equality check, the assume-audit, or the sync divergence report catching a silently vacuous proof.
This is a destructive, adversarial skill. It only ever operates on a dedicated
poison-verif-<component-name> branch that is not meant to be merged. Do not run it on
unstable, a feature branch you intend to ship, or any branch with uncommitted work you
care about.
The component name is $0 and the number of poisons to introduce is $1.
1. Resolve parameters and find the verification artifacts
- If
$0(component name) was not supplied, interactively ask the user which component to poison and stop until provided. - Resolve
$0to exactly one component directory undercomponents/(the lower-case, hyphen-ized form of the name, or the directory whosedefine_component!block declares it). If it does not exist or is ambiguous, return an error and stop. - Discover the component's verification artifacts — at least one must exist, or there is
nothing to desynchronize:
- Creusot mirror: a co-located
verif/crate (e.g.components/<name>/verif/), a*-verifcrate, or anysrc/**file that usescreusot_std::prelude/#[requires]/#[ensures]/pearlite!. Look for awhy3find.json. - Kani harnesses: a
#[cfg(kani)] mod verificationblock (rg 'cfg\(kani\)|kani::proof' <component-dir>), plus any#[cfg(kani)]stubs. - Spin model: a directory
modelling/spin/<name>/with a.pmlfile and aREADME.mdcontaining a "Correspondence to Source Code" table. If none of these exist, stop and report the searched paths — this skill needs abstract code to misalign against. (If the user wants spec↔code poisons instead, point them atcomponent-poison.)
- Creusot mirror: a co-located
- Record which artifact form(s) are present; the poison categories in §3 are keyed to them.
- If
$1(poison-count) was not supplied, interactively ask the user how many poisons to introduce and stop until provided. Require a positive integer; reject0or negatives. If$1exceeds the number of distinct, independently-detectable claims the artifacts expose, warn and offer to cap it.
2. Create and check out the poison branch
- Confirm the working tree is clean enough to proceed. If there are uncommitted changes that would be swept onto the new branch, warn the user and ask whether to continue.
- Record the current branch as the base branch.
- Create a new local branch derived from the current branch, named
poison-verif-<component-name>(using the lower-case, hyphen-ized component name), and check it out — e.g.git checkout -b poison-verif-dispatch-map. If a branch with that name already exists, ask the user whether to delete and recreate it, or to append a numeric suffix. Do not push this branch to any remote.
3. Introduce $1 verif poisons
Build a short inventory of what each verification artifact claims about the concrete code:
the mirrored function(s) and their bodies, each #[requires]/#[ensures]/loop
#[invariant], each kani::assume/assert, each Promela property (P1, P2, …) and its
correspondence rows. Each poison must target a specific, testable claim. Spread the poisons
across different claims and, where possible, across different categories below so the test
exercises several detection paths.
The essential axis is which side you poison and whether the mismatch is loud or silent:
- Loud — the mismatch makes a VC go red (Creusot
✘), a Kani harness fail, or Spin produce a counterexample. Easy to detect; confirms the tool runs. - Silent — the mismatch leaves every proof green but the proof no longer means what it
claims (vacuous contract, drifted mirror, unmatched assume, stale correspondence). Only the
drift/equality check, the assume-audit, or the sync divergence report catches it.
These are the high-value poisons — they test whether the harness is actually bound to
the shipped code. Include at least one silent poison when
$1 >= 2.
Choose from these categories (mix them; do not use the same category for every poison):
Creusot (mirror crate)
- Mirror-body drift — change the mirror function body in
verif/so it no longer matches the shipped function (or change the shipped function so it no longer matches the mirror), without touching the contract. The proof stays green; the drift/equality check should flag that shipped ≠ mirror. (→ Drift / Contradiction, silent.) - Vacuous contract — weaken an
#[ensures]to something trivially true (e.g.result@ >= 0,true, or dropping the tie to*entry/^entry) so the VC passes without constraining behavior. Fault injection should reveal the VC staysProved ✔when it must go red. (→ Vacuous / Unverifiable, silent.) - Precondition mismatch — strengthen a
#[requires]beyond what the production guard ensures (assumes a condition the caller never establishes), or drop a#[requires]the body actually depends on (proof now fails). (→ Contradiction loud, or Unsound precondition silent.) - Postcondition contradiction — flip an
#[ensures]to state the opposite of what the body computes (e.g.^entry.read_ref == *entry.read_ref - 1u32where the body adds). The VC should go red. (→ Contradiction, loud.) - Type/structure drift in the mirror — change a mirrored field/return type so it diverges
from the real struct (
u32where the shipped field isu64,Option<T>vsT, a dropped enum variant ofLocation). (→ Contradiction, may be loud or silent.)
Kani (#[cfg(kani)] mod verification)
- Unmatched assume — add or keep a
kani::assume(X)for which no production guard enforcesX(e.g.assume(read_ref < u32::MAX)while the shipped increment is a bare+= 1). The harness passes in a restricted universe; the Phase-4 assume-audit should flag it as hiding a bug. (→ Unsound assumption, silent.) - Harness detached from code — rewrite the harness to assert against a local copy / reimplementation instead of calling the real function, so injecting a fault into the shipped function no longer fails the harness. (→ Drift, silent.)
- Stub divergence — change a
#[cfg(kani)]stub so its behavior/API subtly differs from the#[cfg(not(kani))]production type (different default, different bound), verifying a type the shipped build never uses. (→ Drift / Contradiction.) - Wrong postcondition — change an
assert!to encode a property the concrete code does not actually satisfy (loud: counterexample) or a trivially-true one (silent: vacuous). - Coverage gap — delete a harness for a still-present function whose invariant matters, or add an unverified new arithmetic site to the shipped code that no harness covers. (→ Missing coverage, silent unless a completeness check runs.)
Spin (modelling/spin/<name>/)
- Protocol drift — change the concrete code's protocol (lock ordering, a new error branch,
a new transition, a changed channel semantic) without updating the
.pml, so the model no longer reflects source.tools-spin-syncshould report MODEL UPDATE REQUIRED. (→ Model drift, silent until sync runs.) - Model weakening — remove or weaken a Promela property (
P_n) or anassertin the.pmlso a genuinely-reachable violation is no longer checked. (→ Vacuous property.) - Correspondence rot — edit the shipped logic so the README "Correspondence to Source Code" line ranges/function names are wrong (LINE DRIFT or worse). (→ Stale correspondence.)
Guidance for each poison:
- Make it a semantic mismatch between concrete and abstract, not a cosmetic one. Renaming a private local or reflowing comments will not exercise the drift tooling. Target something the verification artifact states as an observable contract, invariant, or property.
- Keep each poison small and self-contained, and note the exact
path:lineon both the concrete side and the abstract side. - Prefer targeting distinct functions / contracts / properties so the poisons are independently detectable.
- For each poison, decide up front whether it is loud or silent and record the mechanism you expect to catch it (VC red, counterexample, drift/equality check, assume-audit, or sync divergence). A poison whose detection path you cannot name is a weak test — replace it.
4. Keep the concrete code compiling and the artifacts runnable
Two things must hold after all poison changes:
The shipped code must still compile. Build the component with its default configuration:
cargo build -p <crate-name>The verification artifacts must still parse/typecheck, so the detection tool actually runs and reports a mismatch rather than dying on a syntax/compile error (a build failure is not a drift finding). Sanity-check whichever forms are present, without "helpfully" reconciling the mismatch:
- Creusot:
cargo creusot --only coma(syntax check, no SMT) in theverif/crate. - Kani:
cargo build --features kani-equivalent orcargo kani --only-codegenif available, otherwise confirm the#[cfg(kani)]module compiles undercfg(kani). - Spin: confirm the
.pmlstill compiles withspin -a(ormakeup to codegen) — but do not run the full model check here (that is the detection step, run later by the user).
- Creusot:
If a component is SPDK-gated and cannot be built in this environment, build with its default
configuration and clearly note in the report that a full --workspace/hardware build was not
verified. If a poison cannot be made to compile/parse without also removing the misalignment,
back it out and choose a different poison so the final count of applied poisons still equals
$1 (or, if impossible, stop and report how many were successfully applied).
Do not run formatters or linters that would reconcile the mismatch, and do not run
tools-verify-creusot, tools-verify-kani, tools-spin-sync, component-sync-specs, or any
sync/repair skill — running those is the test, and doing so here would defeat the purpose.
5. Summarize the poisons
Write a POISON-VERIF.md file in the component directory summarizing every applied change in a
table, so the poisons can later be compared against whatever the verification tooling detects
(the ground-truth key). Use this shape:
# Verif Poison Report
Component: <component-dir>
Verification forms present: <Creusot verif/ | Kani mod verification | Spin model>
Base branch: <base branch>
Poison branch: poison-verif-<component-name>
Poisons requested: <$1> Poisons applied: <N> (loud: <x>, silent: <y>)
Concrete build verified: <cargo build -p <crate> result / caveat>
Artifacts parse-checked: <creusot --only coma / kani codegen / spin -a result / caveat>
> Generated by the `component-poison-verif` skill to test formal-verification drift tooling.
> This branch is intentionally broken and must not be merged.
| # | Form | Category | Side poisoned | Abstract claim (path:line) | Concrete location (path:line) | What was changed | Loud/Silent | Detection mechanism | Expected result |
|---|---|---|---|---|---|---|---|---|---|
| 1 | Creusot | Vacuous contract | abstract | verif/src/lib.rs:182 | src/lib.rs:184 | `#[ensures(^e.read_ref == *e.read_ref+1)]` → `#[ensures(true)]` | Silent | fault injection keeps VC green | Vacuous (must go red) |
| 2 | Kani | Unmatched assume | abstract | src/lib.rs:220 (harness) | src/lib.rs:96 (bare `+= 1`) | added `assume(read_ref < MAX)` with no guard | Silent | assume-audit | Unsound assumption |
| 3 | Creusot | Mirror-body drift | concrete | verif/src/lib.rs:188 | src/dispatch.rs:142 | shipped `checked_add` → `wrapping_add` | Silent | drift/equality check | Drift (shipped ≠ mirror) |
| 4 | Spin | Model weakening | abstract | modelling/spin/…/m.pml:57 (P2) | src/state.rs:70 | removed `assert(write_ref<=1)` | Loud on real bug / Silent | tools-spin-sync | Vacuous property |
Map the "Expected result" column to the vocabulary the relevant detection path uses (VC
Proved ✔/✘, Kani failed/assume-audit defect, Spin PASS/FAIL/MODEL UPDATE REQUIRED, or
the drift/equality-check verdict) so the poison report doubles as an answer key.
6. Report
Print a concise summary to the user: the poison branch name, the base branch, which
verification form(s) were poisoned, how many poisons were applied (vs requested) and the
loud/silent split, whether the concrete build passed, whether the artifacts still parse, and
the path to POISON-VERIF.md. Remind the user this branch is intentionally broken, must not be
merged, and that they can now run the matching verification skill against the component to see
how many poisons it catches:
- Creusot poisons →
tools-verify-creusot(watch for VCs that should go red but stay green — the drift/equality check and fault-injection step are what catch the silent ones). - Kani poisons →
tools-verify-kani(the Phase-4 assume-audit is what catches unmatched assumes and detached harnesses). - Spin poisons →
tools-spin-sync(the divergence report is what catches protocol drift and stale correspondence).
Do not commit or push unless the user explicitly asks.