Linearizability: Lazy List Set (NuSMV)¶
Description of the Case Study¶
This case study encodes the lazy list set algorithm of Heller, Herlihy, Shavit, and Tzafrir [HHST05] in NuSMV. Two threads traverse a sorted linked list while holding fine-grained locks to insert, remove, or lookup a single shared key. Logical deletion followed by physical removal makes it possible for one thread to observe stale membership information when a second thread interleaves between validation and commit. Linearizability [HW90] demands that every concurrent history of these operations corresponds to some sequential history that contains the same invocations and responses.
The concurrent model captures the standard lazy-list phases—traverse, lock predecessor and current nodes, validate links, and then perform the update—along with boolean return values for add, remove, and contains. The sequential specification executes the same operations atomically over the abstract set. Using pessimistic semantics, a bounded counterexample suffices to refute linearizability. HyperQB reports SAT for this benchmark, producing a witness where a lookup returns stale information because validation passed just before another thread removed the target node, reproducing the classic lazy-list violation.
The NuSMV model(s)¶
MODULE main
VAR
-- Shared lazy list structure: HEAD -> (maybe X) -> TAIL
ptrh : 0..2; -- head's next-pointer (0=head, 1=X, 2=tail)
ptrx : 0..2; -- X's next-pointer
ptrt : 0..2; -- tail's next-pointer
marked : 0..1; -- logical deletion flag for X (1=absent, 0=present)
-- Fine-grained locking mechanism
l_1 : 0..1; -- HEAD lock (0=unlocked, 1=locked)
l_2 : 0..1; -- X lock (0=unlocked, 1=locked)
l_3 : 0..1; -- TAIL lock (0=unlocked, 1=locked)
-- Concurrency control
stutter : 0..1; -- process scheduler (0=p1 turn, 1=p2 turn)
ret_delay : 0..2; -- return delay to synchronize with sequential model
-- Process 1: Concurrent lazy list operations
p1_pc : 0..7; -- program counter (0=select, 1=find, 2=lock_pred, 3=lock_curr, 4=validate, 5=execute, 6=unlock, 7=return)
p1_pred : 0..2; -- predecessor node pointer
p1_curr : 0..2; -- current node pointer
p1_op : 0..2; -- operation type {ADD=0, REMOVE=1, LOOKUP=2}
p1_enq_ret : 0..1; -- ADD operation return value
p1_deq_ret : 0..1; -- REMOVE operation return value
p1_look_ret : 0..1; -- LOOKUP operation return value
-- Process 2: Identical structure to Process 1
p2_pc : 0..7; -- program counter
p2_pred : 0..2; -- predecessor node pointer
p2_curr : 0..2; -- current node pointer
p2_op : 0..2; -- operation type {ADD=0, REMOVE=1, LOOKUP=2}
p2_enq_ret : 0..1; -- ADD operation return value
p2_deq_ret : 0..1; -- REMOVE operation return value
p2_look_ret : 0..1; -- LOOKUP operation return value
ASSIGN
-- initialize list as HEAD->TAIL (X absent
init(ptrh) := 1;
init(ptrx) := 2;
init(ptrt) := 2;
init(marked) := 0;
init(l_1) := 0;
init(l_2) := 0;
init(l_3) := 0;
-- Thread 1 (REMOVE)
init(p1_pc) := 0;
init(p1_pred) := 0;
init(p1_curr) := 0;
-- init(p1_op_sub1) := 0; -- choose REMOVE
-- init(p1_op_sub2) := 0;
init(p1_op) := 0;
init(p1_enq_ret) := 0;
init(p1_deq_ret) := 0;
init(p1_look_ret) := 0;
-- Thread 2 (LOOKUP)
init(p2_pc) := 0;
init(p2_pred) := 0;
init(p2_curr) := 0;
-- init(p2_op_sub1) := 0; -- choose LOOKUP
-- init(p2_op_sub2) := 0;
init(p2_op) := 0;
init(p2_enq_ret) := 0;
init(p2_deq_ret) := 0;
init(p2_look_ret) := 0;
init(stutter) := 0;
init(ret_delay) := 0;
----------------------------------------------------------------
next(stutter) := case
stutter = 0 : 1; -- stutter on
stutter = 1 : 0; -- stutter off
TRUE : stutter; -- keep current value
esac;
next(ret_delay) := case
ret_delay = 0 & (p1_pc = 7 | p2_pc = 7) : 1; -- delay return for one step
ret_delay = 1 : 2;
ret_delay = 2 : 0; -- reset delay
TRUE : ret_delay; -- keep current value
esac;
next(p1_op) := case
p1_pc = 0 & stutter = 1 : {0,1,2}; -- choose operation ADD/REMOVE/LOOKUP
TRUE : p1_op; -- keep current value
esac;
-- proc1 state-machine
next(p1_pc) := case
(stutter = 0) & p1_pc = 0 : 1; -- find
(stutter = 0) & p1_pc = 1 & p1_op = 2 : 3;
(stutter = 0) & p1_pc = 1 & ((p1_pred = 0 & l_1 = 0) | (p1_pred = 1 & l_2 = 0) | (p1_pred = 2 & l_3 = 0)) : 2; -- lock pred success
(stutter = 0) & p1_pc = 1 & ((p1_pred = 0 & l_1 = 1) | (p1_pred = 1 & l_2 = 1) | (p1_pred = 2 & l_3 = 1)) : 1; -- lock pred fail
(stutter = 0) & p1_pc = 2 & ((p1_curr = 0 & l_1 = 0) | (p1_curr = 1 & l_2 = 0) | (p1_curr = 2 & l_3 = 0)) : 3; -- lock curr success
(stutter = 0) & p1_pc = 2 & ((p1_curr = 0 & l_1 = 1) | (p1_curr = 1 & l_2 = 1) | (p1_curr = 2 & l_3 = 1)) : 2; -- lock curr fail
(stutter = 0) & p1_pc = 3 : 4; -- validate
(stutter = 0) & p1_pc = 4 & ((p1_pred = 0 & ptrh = p1_curr) | (p1_pred = 1 & ptrx = p1_curr) | (p1_pred = 2 & ptrt = p1_curr)) : 5;
(stutter = 0) & p1_pc = 4 & !((p1_pred = 0 & ptrh = p1_curr) | (p1_pred = 1 & ptrx = p1_curr) | (p1_pred = 2 & ptrt = p1_curr)) : 6;
(stutter = 0) & p1_pc = 5 : {5, 6};
(stutter = 0) & p1_pc = 6 & (ret_delay = 0) : 7; -- unlock
(stutter = 0) & p1_pc = 6 : 6; -- unlock
(stutter = 0) & p1_pc = 7 : 0; -- done
TRUE : p1_pc;
esac;
-- find: advance pred/curr until curr.key >= X (X=1, TAIL=2)
next(p1_pred) := case
(p1_pc = 1) & (stutter = 0): p1_curr;
TRUE : p1_pred;
esac;
next(p1_curr) := case
(p1_pc = 1) & (stutter = 0) & p1_curr = 0 : ptrh;
(p1_pc = 1) & (stutter = 0) & p1_curr = 1 : ptrx;
(p1_pc = 1) & (stutter = 0) & p1_curr = 2 : ptrt;
TRUE : p1_curr;
esac;
-- Locks update
next(l_1) := case
(p1_op != 2) & (p2_op != 2) & ((p1_pc = 2 & p1_pred = 0) | (p2_pc = 2 & p2_pred = 0)) : 1;
(p1_op != 2) & (p2_op != 2) & ((p1_pc = 3 & p1_curr = 0) | (p2_pc = 3 & p2_curr = 0)) : 1;
(p1_pc = 6 | p2_pc = 6) & (ret_delay = 0) : 0;
TRUE : l_1;
esac;
next(l_2) := case
(p1_op != 2) & (p2_op != 2) & ((p1_pc = 2 & p1_pred = 1) | (p2_pc = 2 & p2_pred = 1)) : 1;
(p1_op != 2) & (p2_op != 2) & ((p1_pc = 3 & p1_curr = 1) | (p2_pc = 3 & p2_curr = 1)) : 1;
(p1_pc = 6 | p2_pc = 6) & (ret_delay = 0) : 0;
TRUE : l_2;
esac;
next(l_3) := case
(p1_op != 2) & (p2_op != 2) & ((p1_pc = 2 & p1_pred = 2) | (p2_pc = 2 & p2_pred = 2)) : 1;
(p1_op != 2) & (p2_op != 2) & ((p1_pc = 3 & p1_curr = 2) | (p2_pc = 3 & p2_curr = 2)) : 1;
(p1_pc = 6 | p2_pc = 6) & (ret_delay = 0) : 0;
TRUE : l_3;
esac;
-- perform the chosen operation in state 5
next(marked) := case
(p1_pc = 5 & p1_op = 0) | (p2_pc = 5 & p2_op = 0) : 0;
(p1_pc = 5 & p1_op = 1) | (p2_pc = 5 & p2_op = 1) : 1;
TRUE : marked;
esac;
-- update head pointer only
next(ptrh) := case
(p1_pc = 6 & p1_op = 0) | (p2_pc = 6 & p2_op = 0) : 1;
(p1_pc = 6 & p1_op = 1) | (p2_pc = 6 & p2_op = 1) : 2;
TRUE : ptrh;
esac;
next(ptrx) := ptrx;
next(ptrt) := ptrt;
-- record return values for proc1
next(p1_enq_ret) := case
p1_pc = 6 & p1_op = 0 & (p1_curr != 1) & (stutter = 0) & (ret_delay = 0): 1;
TRUE : 0;
esac;
next(p1_deq_ret) := case
p1_pc = 6 & p1_op = 1 & (p1_curr = 1) & (stutter = 0) & (ret_delay = 0): 1;
TRUE : 0;
esac;
next(p1_look_ret) := case
p1_pc = 6 & p1_op = 2 & (p1_curr = 1) & (marked = 0) & (stutter = 0) & (ret_delay = 0): 1;
p1_pc = 6 & p1_op = 2 & ((p1_curr != 1) | (marked = 1)) : 0;
TRUE : 0;
esac;
next(p2_op) := case
p2_pc = 0 & stutter = 0 : {0,1,2}; -- choose operation ADD/REMOVE/LOOKUP
TRUE : p2_op; -- keep current value
esac;
-- proc2 state-machine
next(p2_pc) := case
(stutter = 1) & p2_pc = 0 : 1;
(stutter = 1) & p2_pc = 1 & p2_op = 2 : 3;
(stutter = 1) & p2_pc = 1 & ((p2_pred = 0 & l_1 = 0) | (p2_pred = 1 & l_2 = 0) | (p2_pred = 2 & l_3 = 0)) : 2;
(stutter = 1) & p2_pc = 1 & ((p2_pred = 0 & l_1 = 1) | (p2_pred = 1 & l_2 = 1) | (p2_pred = 2 & l_3 = 1)) : 1;
(stutter = 1) & p2_pc = 2 & ((p2_curr = 0 & l_1 = 0) | (p2_curr = 1 & l_2 = 0) | (p2_curr = 2 & l_3 = 0)) : 3;
(stutter = 1) & p2_pc = 2 & ((p2_curr = 0 & l_1 = 1) | (p2_curr = 1 & l_2 = 1) | (p2_curr = 2 & l_3 = 1)) : 2;
(stutter = 1) & p2_pc = 3 : {3, 4};
(stutter = 1) & p2_pc = 4 & ((p2_pred = 0 & ptrh = p2_curr) | (p2_pred = 1 & ptrx = p2_curr) | (p2_pred = 2 & ptrt = p2_curr)) : 5;
(stutter = 1) & p2_pc = 4 & !((p2_pred = 0 & ptrh = p2_curr) | (p2_pred = 1 & ptrx = p2_curr) | (p2_pred = 2 & ptrt = p2_curr)): 6;
(stutter = 1) & p2_pc = 5 : 6;
(stutter = 1) & p2_pc = 6 & (ret_delay = 0) : 7;
(stutter = 1) & p2_pc = 6 : 6;
(stutter = 1) & p2_pc = 7 : 0;
TRUE : p2_pc;
esac;
next(p2_pred) := case
(p2_pc = 1) & (stutter = 1) : p2_curr;
TRUE : p2_pred;
esac;
next(p2_curr) := case
(p2_pc = 1) & (stutter = 1) & p2_pred = 0 : ptrh;
(p2_pc = 1) & (stutter = 1) & p2_pred = 1 : ptrx;
(p2_pc = 1) & (stutter = 1) & p2_pred = 2 : ptrt;
TRUE : p2_curr;
esac;
-- record return values for proc2
next(p2_enq_ret) := case
p2_pc = 6 & p2_op = 0 & (p2_curr != 1) & (stutter = 1) & (ret_delay = 0): 1;
TRUE : 0;
esac;
next(p2_deq_ret) := case
p2_pc = 6 & p2_op = 1 & (p2_curr = 1) & (stutter = 1) & (ret_delay = 0): 1;
TRUE : 0;
esac;
next(p2_look_ret) := case
p2_pc = 6 & p2_op = 2 & (p2_curr = 1) & (marked = 0) & (stutter = 1) & (ret_delay = 0): 1;
TRUE : 0;
esac;
DEFINE
p1_enq_envoked := (p1_op = 0) & ((p1_pc = 1) | (p1_pc = 2) | (p1_pc = 3) | (p1_pc = 4) | (p1_pc = 5) | (p1_pc = 6));
p2_enq_envoked := (p2_op = 0) & ((p2_pc = 1) | (p2_pc = 2) | (p2_pc = 3) | (p2_pc = 4) | (p2_pc = 5) | (p2_pc = 6));
p1_deq_envoked := (p1_op = 1) & ((p1_pc = 1) | (p1_pc = 2) | (p1_pc = 3) | (p1_pc = 4) | (p1_pc = 5) | (p1_pc = 6));
p2_deq_envoked := (p2_op = 1) & ((p2_pc = 1) | (p2_pc = 2) | (p2_pc = 3) | (p2_pc = 4) | (p2_pc = 5) | (p2_pc = 6));
p1_look_envoked := (p1_op = 2) & ((p1_pc = 1) | (p1_pc = 2) | (p1_pc = 3) | (p1_pc = 4) | (p1_pc = 5) | (p1_pc = 6));
p2_look_envoked := (p2_op = 2) & ((p2_pc = 1) | (p2_pc = 2) | (p2_pc = 3) | (p2_pc = 4) | (p2_pc = 5) | (p2_pc = 6));
envoked_add := (p1_enq_envoked | p2_enq_envoked);
envoked_pop := (p1_deq_envoked | p2_deq_envoked);
envoked_look := (p1_look_envoked | p2_look_envoked);
returning_add := (p1_op = 0 & p1_pc = 7) | (p2_op = 0 & p2_pc = 7);
returning_pop := (p1_op = 1 & p1_pc = 7) | (p2_op = 1 & p2_pc = 7);
returning_look := (p1_op = 2 & p1_pc = 7) | (p2_op = 2 & p2_pc = 7);
add_return := ((p1_enq_ret = 1) | (p2_enq_ret = 1));
pop_return := ((p1_deq_ret = 1) | (p2_deq_ret = 1));
look_return := ((p1_look_ret = 1) | (p2_look_ret = 1));
MODULE main
VAR
-- shared list: HEAD->(maybe X)->TAIL
marked : 0..1; -- logical deletion flag for X (1=absent,0=present)
busy : 0..1; -- busy flag (0=free, 1=busy)
stutter : 0..1; -- stutter flag (0=p1 turn, 1=p2 turn)
-- Process 1
p1_pc : 0..2; -- {0=select,1=spin,2=do+return}
p1_op : 0..3; -- {ADD=0, REMOVE=1, LOOKUP=2, NONE=3}
p1_enq_ret : 0..1;
p1_deq_ret : 0..1;
p1_look_ret : 0..1;
-- Process 2
p2_pc : 0..2; -- {0=select,1=spin,2=do+return}
p2_op : 0..3; -- {ADD=0, REMOVE=1, LOOKUP=2, NONE=3}
p2_enq_ret : 0..1;
p2_deq_ret : 0..1;
p2_look_ret : 0..1;
ASSIGN
-- initial states
init(marked) := 0;
init(busy) := 0;
init(stutter) := 0;
init(p1_pc) := 0;
init(p1_op) := 3; -- NONE
init(p1_enq_ret) := 0;
init(p1_deq_ret) := 0;
init(p1_look_ret):= 0;
init(p2_pc) := 0;
init(p2_op) := 3; --NONE
init(p2_enq_ret) := 0;
init(p2_deq_ret) := 0;
init(p2_look_ret):= 0;
-- Stutter control: alternates between processes
next(stutter) := case
stutter = 0 : 1; -- switch to p2 turn
stutter = 1 : 0; -- switch to p1 turn
TRUE : stutter;
esac;
-- Busy flag management
next(busy) := case
-- Set busy when either process starts modifying operation (ADD/REMOVE)
(p1_pc = 2 & (p1_op = 0 | p1_op = 1)) | (p2_pc = 2 & (p2_op = 0 | p2_op = 1)) : 1;
-- Release busy when both processes are not modifying
!((p1_pc = 2 & (p1_op = 0 | p1_op = 1)) | (p2_pc = 2 & (p2_op = 0 | p2_op = 1))) : 0;
TRUE : busy;
esac;
-- Process 1 operations (only when stutter = 0)
next(p1_op) := case
p1_pc = 0 & stutter = 0 : {0, 1, 2};
TRUE : p1_op;
esac;
-- Process 2 operations (only when stutter = 1)
next(p2_op) := case
p2_pc = 0 & stutter = 1 : {0, 1, 2};
TRUE : p2_op;
esac;
next(marked) := case
-- Only allow modification if it's the process's turn and not busy
p1_pc = 2 & p1_op = 0 & stutter = 0 & (busy = 0 | !(p2_pc = 2 & (p2_op = 0 | p2_op = 1))) : 0; -- P1 ADD: mark present
p1_pc = 2 & p1_op = 1 & stutter = 0 & (busy = 0 | !(p2_pc = 2 & (p2_op = 0 | p2_op = 1))) : 1; -- P1 REMOVE: mark absent
p2_pc = 2 & p2_op = 0 & stutter = 1 & (busy = 0 | !(p1_pc = 2 & (p1_op = 0 | p1_op = 1))) : 0; -- P2 ADD: mark present
p2_pc = 2 & p2_op = 1 & stutter = 1 & (busy = 0 | !(p1_pc = 2 & (p1_op = 0 | p1_op = 1))) : 1; -- P2 REMOVE: mark absent
TRUE : marked;
esac;
-- Process 1 return values (only when stutter = 0)
next(p1_enq_ret) := case
p1_pc = 2 & p1_op = 0 & stutter = 0 & marked != 1 & (busy = 0 | !(p2_pc = 2 & (p2_op = 0 | p2_op = 1))) : 1; -- ADD succeeded
TRUE : 0;
esac;
next(p1_deq_ret) := case
p1_pc = 2 & p1_op = 1 & stutter = 0 & marked != 0 & (busy = 0 | !(p2_pc = 2 & (p2_op = 0 | p2_op = 1))) : 1; -- REMOVE found
TRUE : 0;
esac;
next(p1_look_ret) := case
p1_pc = 2 & p1_op = 2 & stutter = 0 & marked = 0 : 1; -- LOOKUP sees X
TRUE : 0;
esac;
-- Process 2 return values (only when stutter = 1)
next(p2_enq_ret) := case
p2_pc = 2 & p2_op = 0 & stutter = 1 & marked != 1 & (busy = 0 | !(p1_pc = 2 & (p1_op = 0 | p1_op = 1))) : 1; -- ADD succeeded
TRUE : 0;
esac;
next(p2_deq_ret) := case
p2_pc = 2 & p2_op = 1 & stutter = 1 & marked != 0 & (busy = 0 | !(p1_pc = 2 & (p1_op = 0 | p1_op = 1))) : 1; -- REMOVE found
TRUE : 0;
esac;
next(p2_look_ret) := case
p2_pc = 2 & p2_op = 2 & stutter = 1 & marked = 0 : 1; -- LOOKUP sees X
TRUE : 0;
esac;
-- PC transition: 0 -> {1,2} -> 2 -> 0 (Process 1 only moves when stutter = 0)
next(p1_pc) := case
p1_pc = 0 & stutter = 0 : 1;
p1_pc = 1 & stutter = 0 : {1, 2};
p1_pc = 2 & stutter = 0 : 0;
TRUE : p1_pc;
esac;
-- PC transition: 0 -> {1,2} -> 2 -> 0 (Process 2 only moves when stutter = 1)
next(p2_pc) := case
p2_pc = 0 & stutter = 1 : 1;
p2_pc = 1 & stutter = 1 : {1, 2};
p2_pc = 2 & stutter = 1 : 0;
TRUE : p2_pc;
esac;
DEFINE
envoked_add := ((p1_op = 0) & ((p1_pc = 1) | (p1_pc = 2))) | ((p2_op = 0) & ((p2_pc = 1) | (p2_pc = 2)));
envoked_pop := ((p1_op = 1) & ((p1_pc = 1) | (p1_pc = 2))) | ((p2_op = 1) & ((p2_pc = 1) | (p2_pc = 2)));
envoked_look := ((p1_op = 2) & ((p1_pc = 1) | (p1_pc = 2))) | ((p2_op = 2) & ((p2_pc = 1) | (p2_pc = 2)));
returning_add := ((p1_op = 0) & (p1_pc = 0)) | ((p2_op = 0) & (p2_pc = 0));
returning_pop := ((p1_op = 1) & (p1_pc = 0)) | ((p2_op = 1) & (p2_pc = 0));
returning_look := ((p1_op = 2) & (p1_pc = 0)) | ((p2_op = 2) & (p2_pc = 0));
add_return := (p1_enq_ret = 1) | (p2_enq_ret = 1);
pop_return := (p1_deq_ret = 1) | (p2_deq_ret = 1);
look_return := (p1_look_ret = 1) | (p2_look_ret = 1);
The HyperLTL formula(s)¶
Linearizability is expressed by equating the invocation and response history of the concurrent lazy-list execution with that of a sequential witness. If the concurrent run ever observes a lookup result that cannot be produced sequentially, the formula is violated.
Exists A . Forall B .
~G(((envoked_pop[A]=envoked_pop[B])&(returning_pop[A]=returning_pop[B]))&(pop_return[A]=pop_return[B])&
((envoked_add[A]=envoked_add[B])&(returning_add[A]=returning_add[B]))&(add_return[A]=add_return[B])&
((envoked_look[A]=envoked_look[B])&(returning_look[A]=returning_look[B]))&(look_return[A]=look_return[B])
)