State lock — two concurrent applies can never both hold the lock
Terraform's docs put it bluntly: two `apply` runs against the same state file at once will corrupt it. Teams learn this the hard way — CI kicks off a plan while an engineer applies from a laptop, the state JSON is half-written by both, and suddenly Terraform wants to destroy and recreate half of production. HashiCorp added state locking for exactly this; here the lock's mutual exclusion is a proof, not a DynamoDB conditional write you hope is configured. The lock is a state machine: `unlocked -> held -> unlocked`, with a `stale` branch for the crashed-runner case — force-unlock exists, but ONLY from `stale`, never as a shortcut around a live holder. The holder-count ledger is invariant-walled at 0..1: a second holder is a write the memory refuses, independent of the machine.
A Soma program from the devops part of the verified corpus: 114 lines, cells StateLock, MutexTests. It passes soma check with soma 3.2.4.
How it is built
The cell StateLock declares 5 signals in its face: acquire(ws: String, runner: String) -> Map, release_lock(ws: String) -> Map, mark_stale(ws: String) -> Map, force_unlock(ws: String) -> Map and lock_state(ws: String) -> Map; keeps 2 memory slots: holders (Map<String, Int>, persistent and consistent) under the invariant holders >= 0 && holders <= 1 and owner (Map<String, String>, persistent and consistent). Its state machine tflock starts in unlocked and allows 4 transitions: unlocked -> held, held -> unlocked, held -> stale and stale -> unlocked. Handlers: acquire (line 46), release_lock (line 61), mark_stale (line 69), force_unlock (line 75) and lock_state (line 85).
The file also holds 1 test cell (MutexTests) with 11 assertions, run by soma test.
The source
state_lock.cell
// ============================================================================
// STATE LOCK — two concurrent applies can never both hold the lock
// ============================================================================
//
// Terraform's docs put it bluntly: two `apply` runs against the same
// state file at once will corrupt it. Teams learn this the hard way —
// CI kicks off a plan while an engineer applies from a laptop, the
// state JSON is half-written by both, and suddenly Terraform wants to
// destroy and recreate half of production. HashiCorp added state
// locking for exactly this; here the lock's mutual exclusion is a
// proof, not a DynamoDB conditional write you hope is configured.
//
// The lock is a state machine: `unlocked -> held -> unlocked`, with a
// `stale` branch for the crashed-runner case — force-unlock exists,
// but ONLY from `stale`, never as a shortcut around a live holder.
// The holder-count ledger is invariant-walled at 0..1: a second holder
// is a write the memory refuses, independent of the machine.
// ============================================================================
cell StateLock {
face {
signal acquire(ws: String, runner: String) -> Map
signal release_lock(ws: String) -> Map
signal mark_stale(ws: String) -> Map
signal force_unlock(ws: String) -> Map
signal lock_state(ws: String) -> Map
}
memory {
holders: Map<String, Int> [persistent, consistent]
invariant holders >= 0 && holders <= 1
owner: Map<String, String> [persistent, consistent]
}
// force_unlock is only reachable through `stale` — there is no
// held -> unlocked side door that skips the release protocol.
state tflock {
initial: unlocked
unlocked -> held
held -> unlocked
held -> stale
stale -> unlocked
}
on acquire(ws: String, runner: String) {
let r = try { transition(ws, "held") }
if r.error != () {
let who = owner.get(ws) ?? "unknown"
return map("ok", false, "reason", "lock held by {who}")
}
let n = holders.get(ws) ?? 0
let w = try { holders.set(ws, n + 1) }
if w.error != () {
return map("ok", false, "reason", "second holder refused: {w.error}")
}
owner.set(ws, runner)
return map("ok", true, "holder", runner)
}
on release_lock(ws: String) {
let r = try { transition(ws, "unlocked") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
holders.set(ws, 0)
owner.set(ws, "")
return map("ok", true, "state", "unlocked")
}
on mark_stale(ws: String) {
let r = try { transition(ws, "stale") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", "stale")
}
on force_unlock(ws: String) {
if get_status(ws) != "stale" {
return map("ok", false, "reason", "force-unlock only from stale — a live holder is never bulldozed")
}
transition(ws, "unlocked")
holders.set(ws, 0)
owner.set(ws, "")
return map("ok", true, "state", "unlocked")
}
on lock_state(ws: String) {
return map("state", get_status(ws), "owner", owner.get(ws) ?? "")
}
}
cell test MutexTests {
rules {
// CI takes the lock; the laptop apply is refused
assert acquire("prod", "ci-runner-7").ok == true
assert acquire("prod", "laptop-jane").ok == false
assert lock_state("prod").owner == "ci-runner-7"
// the second-holder write itself is impossible
assert_fails holders.set("prod", 2)
// a live holder cannot be bulldozed — force path needs `stale`
assert force_unlock("prod").ok == false
assert_fails transition("prod", "force_released")
// crashed runner: stale, THEN force-unlock, then life goes on
assert mark_stale("prod").ok == true
assert force_unlock("prod").ok == true
assert acquire("prod", "laptop-jane").ok == true
assert release_lock("prod").ok == true
// an unlocked lock cannot be double-released
assert release_lock("prod").ok == false
}
}What the toolchain says
$ soma check state_lock.cell
✓ All checks passed.$ soma test state_lock.cell
test MutexTests ...
✓ assert acquire("prod", "ci-runner-7").ok == true
✓ assert acquire("prod", "laptop-jane").ok == false
✓ assert lock_state("prod").owner == "ci-runner-7"
✓ assert_fails holders.set("prod", 2) — raised memory invariant violated on 'holders': holders >= 0 && holders <= 1 — rejected write of 2 (key "prod"); the slot is unchanged
✓ assert force_unlock("prod").ok == false
✓ assert_fails transition("prod", "force_released") — raised invalid transition: held → force_released. Current state: 'held'. Valid targets: [unlocked, stale]
✓ assert mark_stale("prod").ok == true
✓ assert force_unlock("prod").ok == true
✓ assert acquire("prod", "laptop-jane").ok == true
✓ assert release_lock("prod").ok == true
✓ assert release_lock("prod").ok == false
11 tests: 11 passed, 0 failed$ soma verify state_lock.cell
Verifying state_lock.cell...
State machine 'tflock': 3 states, initial 'unlocked'
States: [held, stale, unlocked]
✓ 3 states, 4 transitions
✓ all states reachable from 'unlocked'
✓ reactive machine: no terminal state, and every reachable state can return to 'unlocked'
✓ no deadlocks
✓ liveness: every reachable state can return to 'unlocked' (reactive machine)
✓ think-isolated: CTL safety properties hold regardless of LLM output (4 handlers, 4 literal transitions, 0 dynamic)
✓ termination: all 5 handlers structurally terminate
✓ refinement: handler `acquire` ⟶ {held}
✓ refinement: handler `release_lock` ⟶ {unlocked}
✓ refinement: handler `mark_stale` ⟶ {stale}
✓ refinement: handler `force_unlock` ⟶ {stale → unlocked}
· transition sources: `acquire`, `release_lock`, `mark_stale` call transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)
StateLock/invariants:
✓ invariant holders >= 0 — writer 'acquire' proven by induction (writes n + 1)
✓ invariant holders >= 0 && holders <= 1 — writer 'force_unlock' proven (writes 0)
✓ invariant holders >= 0 && holders <= 1 — writer 'release_lock' proven (writes 0)
⚠ invariant holders >= 0 && holders <= 1 — runtime-checked (computed values): acquire → holders [holders <= 1] because `n + 1` is only known to lie in [1, 2] — narrow it: `require (n + 1) <= 1 else …`
14 passed, 1 warnings, 0 failures
Temporal properties for 'tflock':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK