Linearizability: Explicit Memory Management ABA (NuSMV)¶
Description of the Case Study¶
This case study models the Treiber stack [Tre86] under explicit memory management (EMM). Two threads share a stack implemented over a pair of reusable nodes. Once a pop returns a node to the free list, it becomes immediately available for the next push, recreating the classical ABA scenario. Linearizability [HW90] requires that every concurrent history of push and pop operations can be matched by some sequential execution that preserves invocation and response order.
The concurrent NuSMV model explicitly tracks when each thread invokes, completes, and succeeds at push or pop, while the sequential specification executes the same operations one at a time. Under pessimistic semantics, a bounded counterexample is sufficient to disprove linearizability. HyperQB flags the model as SAT within the given bound, producing a witness where a thread observes a recycled node whose value no longer matches a valid sequential history, exposing the ABA bug that arises without reclamation safeguards.
The NuSMV model(s)¶
-- Two-thread Treiber stack over 2 nodes; free1/free2 work as a free-pool stack emulating (EMM)
-- with an explicit chosen mechanic for selecting which free node to push
MODULE main
VAR
  -- Shared stack top: 0=NULL, 1=NODE1, 2=NODE2
  top         : 0..2;
  -- Free-pool flags: TRUE = node free; FALSE = allocated
  free1       : boolean;
  free2       : boolean;
  -- Each node's next pointer (valid if allocated)
  next1       : 0..2;
  next2       : 0..2;
  -- Node payloads (1 or 2)
  val1        : 1..2;    -- NODE1 payload, initially 1; on reallocate becomes 2
  val2        : 1..2;    -- NODE2 payload, initially 2; on reallocate becomes 1
  --delay
  -- Thread 1 control & locals (with two extra pop steps)
  p1_pc       : 0..7;
  p1_op      : 0..2;    -- operation type {PUSH=0, POP=1, NONE=2}
  p1_old_top  : 0..2;-- Cas variables
  p1_old_next : 0..2;
  p1_ret      : 0..2;
  p1_ret_push : boolean; -- return value for T1 push operation (TRUE=success, FALSE=failure)
  p1_chosen   : 0..2;    -- which node T1 will allocate on its next push (0=none,1 or 2)
  -- Thread 2 control & locals
  p2_pc       : 0..6;    -- 0-start_pop,1-pop,2-return,3-start_push,4-push,5-return_push(done)
  p2_old_top  : 0..2;
  p2_old_next : 0..2;
  p2_ret      : 0..2;    -- return value for T2 pop operation
  p2_ret_push : boolean; -- return value for T2 push operation (TRUE=success, FALSE=failure)
  p2_chosen   : 0..2;    -- which node T2 will allocate on its next push (0=none,1 or 2)
ASSIGN
  -- Initial state: stack = [1->2->NULL]
  init(top)        := 1;
  init(next1)      := 2;
  init(next2)      := 0;
  init(free1)      := FALSE;
  init(free2)      := FALSE;
  init(val1)       := 1;
  init(val2)       := 2;
  init(p1_pc)      := 0;
  init(p1_op)      := 2; -- NONE
  init(p1_old_top) := 0;
  init(p1_old_next):= 0;
  init(p1_ret)     := 0;
  init(p1_ret_push):= FALSE;
  init(p1_chosen)  := 0;
  init(p2_pc)      := 0;
  init(p2_old_top) := 0;
  init(p2_old_next):= 0;
  init(p2_ret)     := 0;
  init(p2_ret_push):= FALSE;
  init(p2_chosen)  := 0;
  -- Update val1 on T2's push CAS; and val2 on T1's push CAS
  next(val1) := case
    p2_pc = 4 & p2_chosen = 1 : {1, 2};
    TRUE                      : val1;
  esac;
  next(val2) := case
    p1_pc = 6 & p1_chosen = 2 : {1, 2};
    TRUE                      : val2;
  esac;
  -- Update shared top
  next(top) := case
    p1_pc = 2 & top = p1_old_top : p1_old_next;
    p1_pc = 2                    : top;
    p2_pc = 1 & top = p2_old_top : p2_old_next;
    p2_pc = 1                    : top;
    p1_pc = 6 & p1_chosen != 0   : p1_chosen;
    p2_pc = 4 & p2_chosen != 0   : p2_chosen;
    TRUE                         : top;
  esac;
  -- Update next1
  next(next1) := case
    p1_pc = 2 & p1_old_top = 1 & top = p1_old_top : 0;
    p2_pc = 1 & p2_old_top = 1 & top = p2_old_top : 0;
    p1_pc = 5 & p1_chosen = 1                     : top;
    p2_pc = 3 & free1                             : top;
    TRUE                                          : next1;
  esac;
  -- Update next2
  next(next2) := case
    p1_pc = 2 & p1_old_top = 2 & top = p1_old_top : 0;
    p2_pc = 1 & p2_old_top = 2 & top = p2_old_top : 0;
    p1_pc = 5 & p1_chosen = 2                     : top;
    p2_pc = 3 & free2                             : top;
    TRUE                                          : next2;
  esac;
  -- Update free1
  next(free1) := case
    p1_pc = 2 & p1_old_top = 1 & top = p1_old_top : TRUE;
    p2_pc = 1 & p2_old_top = 1 & top = p2_old_top : TRUE;
    p1_pc = 6 & p1_chosen = 1                     : FALSE;
    p2_pc = 4 & p2_chosen = 1                     : FALSE;
    TRUE                                          : free1;
  esac;
  -- Update free2
  next(free2) := case
    p1_pc = 2 & p1_old_top = 2 & top = p1_old_top : TRUE;
    p2_pc = 1 & p2_old_top = 2 & top = p2_old_top : TRUE;
    p1_pc = 6 & p1_chosen = 2                     : FALSE;
    p2_pc = 4 & p2_chosen = 2                     : FALSE;
    TRUE                                          : free2;
  esac;
  -- Thread 1 control flow (pop goes through two extra steps before done; push unchanged)
  next(p1_pc) := case
    p1_pc = 0 : {1, 5};
    p1_pc = 1 : {1, 2};  
    p1_pc = 2 : 3;       
    p1_pc = 3 : 4;
    (p1_pc = 4) & (p2_pc = 4) : 4;
    p1_pc = 4 & p2_pc != 4 : 7;       
    p1_pc = 5 & (p2_pc != 5) : 5;
    p1_pc = 5 & (p2_pc = 5) : 6;     
    p1_pc = 6 : 7;      
    TRUE      : 0;       
  esac;
  next(p1_op) := case
    p1_pc = 1 : 1; -- choose PUSH or POP
    p1_pc = 5 : 0; -- choose PUSH or POP
    p1_pc = 7 : 2; -- done
    TRUE      : p1_op;
  esac;
  -- Snapshot for pop (T1)
  next(p1_old_top) := case
    p1_pc = 1                   : top;
    TRUE                        : p1_old_top;
  esac;
  next(p1_old_next) := case
    p1_pc = 1 & top = 1 : next1;
    p1_pc = 1 & top = 2 : next2;
    TRUE                : p1_old_next;
  esac;
  -- Record return (T1) after the two extra pop steps
  next(p1_ret) := case
    p1_pc = 4 & top = p1_old_top & free1  : val2;
    p1_pc = 4 & top = p1_old_top & free2  : 0;
    p1_pc = 4 & top = p1_old_top & !free1 : val1;
    p1_pc = 4 & top != p1_old_top         : 0;
    TRUE                                  : p1_ret;
  esac;
  -- Record return (T1) push operation
  next(p1_ret_push) := case
    p1_pc = 6 & p1_chosen != 0 : TRUE;   -- push succeeds if node was chosen
    p1_pc = 6 & p1_chosen = 0  : FALSE;  -- push fails if no node available
    TRUE                       : p1_ret_push;
  esac;
  -- Choose which node T1 will push at push_load
  next(p1_chosen) := case
    p1_pc = 5 & free2 : 2;
    p1_pc = 5 & free1 : 1;
    TRUE      : p1_chosen;
  esac;
  -- Thread 2 control flow (unchanged)
  next(p2_pc) := case
    p2_pc = 0 : 1;
    p2_pc = 1 : 2;
    p2_pc = 2 : 3;
    p2_pc = 3 : 4;
    p2_pc = 4 : 5;
    TRUE      : 0;
  esac;
  -- Snapshot for pop (T2)
  next(p2_old_top) := case
    p2_pc = 0 : top;
    TRUE      : p2_old_top;
  esac;
  next(p2_old_next) := case
    p2_pc = 0 & top = 1 : next1;
    p2_pc = 0 & top = 2 : next2;
    TRUE : p2_old_next;
  esac;
  -- Record return (T2) after pop cas
  next(p2_ret) := case
    p2_pc = 1 & top = p2_old_top & p2_old_top = 1 & !free1 : val1;
    p2_pc = 1 & top = p2_old_top & p2_old_top = 2 & !free2 : val2;
    p2_pc = 1 & top = p2_old_top & p2_old_top = 1 & free1  : 0;
    p2_pc = 1 & top = p2_old_top & p2_old_top = 2 & free2  : 0;
    p2_pc = 1 & top != p2_old_top                          : 0;
    TRUE                                                   : p2_ret;
  esac;
  -- Record return (T2) push operation
  next(p2_ret_push) := case
    p2_pc = 4 & p2_chosen != 0 : TRUE;   -- push succeeds if node was chosen
    p2_pc = 4 & p2_chosen = 0  : FALSE;  -- push fails if no node available
    TRUE                       : p2_ret_push;
  esac;
  -- Choose which node T2 will push at push_load
  next(p2_chosen) := case
    p2_pc = 3 & free2 : 2;
    p2_pc = 3 & free1 : 1;
    TRUE              : p2_chosen;
  esac;
DEFINE
  -- Thread 1 operation predicates
  p1_push_invoked := (p1_pc = 5) | (p1_pc = 6);  -- push_load or push_cas
  p1_pop_invoked  := (p1_pc = 1) | (p1_pc = 2) | (p1_pc = 3) | (p1_pc = 4);  -- pop_load, pop_cas, pop_extra1, pop_extra2
  -- Thread 2 operation predicates  
  p2_push_invoked := (p2_pc = 3) | (p2_pc = 4);  -- push_load or push_cas
  p2_pop_invoked  := (p2_pc = 0) | (p2_pc = 1);  -- pop_load or pop_cas
  -- Combined operation predicates
  invoked_push := (p1_push_invoked | p2_push_invoked);
  invoked_pop  := (p1_pop_invoked | p2_pop_invoked);
  -- Return predicates
  returning_push := (p1_pc = 7 & p1_op = 0) | (p2_pc = 5);
  returning_pop  := (p1_pc = 7 & p1_op = 1) | (p2_pc = 2);
  -- Success return predicates
  push_return := (p1_ret_push) | (p2_ret_push);
  pop_return  := (p1_ret != 0) | (p2_ret != 0);
-- Sequential Treiber stack with 2 elements matching concurrent version
-- Processes execute sequentially using busy flag and stutter mechanism
MODULE main
VAR
  -- Simple stack state (matching concurrent version)
  val1        : 1..2;    -- node 1 value
  val2        : 1..2;    -- node 2 value
  top_pointer : 0..2;    -- stack top: 0=empty, 1=val1, 2=val2
  busy     : boolean; -- busy flag (TRUE=busy, FALSE=free)
  -- Process 1 control & locals
  p1_pc      : 0..2;    -- 0=idle, 1=execute, 2=done
  p1_op      : 0..2;    -- operation type {PUSH=0, POP=1, NONE=2}
  p1_value   : 1..2;    -- value to push
  p1_ret     : 0..2;    -- return value (0=empty, 1,2=popped value)
  p1_ret_push: boolean; -- return value for push operation
  p1_modified: boolean; -- indicates if process 1 modified the stack
  -- Process 2 control & locals  
  p2_pc      : 0..2;    -- 0=idle, 1=execute, 2=done
  p2_op      : 0..2;    -- operation type {PUSH=0, POP=1, NONE=2}
  p2_value   : 1..2;    -- value to push
  p2_ret     : 0..2;    -- return value (0=empty, 1,2=popped value)
  p2_ret_push: boolean; -- return value for push operation
  p2_modified: boolean; -- indicates if process 2 modified the stack
ASSIGN
  -- initial state: stack = [val1->val2->NULL] (matching concurrent model)
  init(val1)        := 1;
  init(val2)        := 2;
  init(top_pointer) := 1;  -- stack starts with val1 on top
  init(busy)        := TRUE; -- stack is initially free
  next(busy) := case
    p2_pc = 2 : TRUE; -- busy when process 2 is done
    p1_pc = 2 : TRUE;
    TRUE : FALSE; -- free otherwise
  esac;
  -- Process 1 initial state
  init(p1_pc)      := 0;
  init(p1_op)      := 2; -- NONE
  init(p1_value)   := 1;
  init(p1_ret)     := 0;
  init(p1_ret_push):= FALSE;
  init(p1_modified):= FALSE; -- initially not modified
  -- Process 2 initial state
  init(p2_pc)      := 1;
  init(p2_op)      := 1; -- remove
  init(p2_value)   := 2;
  init(p2_ret)     := 0;
  init(p2_ret_push):= FALSE;
  init(p2_modified) := FALSE; -- initially not modified
  next(p2_modified) := case
    p2_pc = 2 : TRUE; -- modified when process 2 is done
    p1_pc = 2 : FALSE; -- not modified otherwise
    TRUE : p2_modified; -- keep current state
  esac;
  next(p1_modified) := case
    p1_pc = 2 : TRUE; -- modified when process 1 is done
    p2_pc = 2 : FALSE; -- not modified otherwise
    TRUE : p1_modified; -- keep current state
  esac;
  -- Process 1 operation selection
  next(p1_op) := case
    p1_pc = 0 : {0, 1}; -- choose PUSH or POP
    TRUE : p1_op;
  esac;
  -- Process 2 operation selection  
  next(p2_op) := case
    p2_pc = 0 : {0, 1}; -- choose PUSH or POP
    TRUE : p2_op;
  esac;
  -- Process 1 value selection for push
  next(p1_value) := case
    p1_pc = 0 : {1, 2}; -- choose value to push
    TRUE : p1_value;
  esac;
  -- Process 2 value selection for push
  next(p2_value) := case
    p2_pc = 0 : {1, 2}; -- choose value to push
    TRUE : p2_value;
  esac;
  -- Process 1 control flow
  next(p1_pc) := case
    p1_pc = 0 : 1; -- idle -> execute
    p1_pc = 1 & busy : 1;
    p1_pc = 1 : {1, 2}; -- execute -> done
    p1_pc = 2 : 0; -- done -> idle
    TRUE : p1_pc;
  esac;
  -- Process 2 control flow
  next(p2_pc) := case
    p2_pc = 0 : 1; -- idle -> execute
    p2_pc = 1 : {1, 2}; -- execute -> done
    p2_pc = 2 : 0; -- done -> idle
    TRUE : p2_pc;
  esac;
  -- Update val1 and val2 when pushed
  next(val1) := case
    p1_pc = 2 & p1_op = 0 & p1_value = 1 & top_pointer = 0 : p1_value; -- P1 pushes to val1
    p2_pc = 2 & p2_op = 0 & p2_value = 1 & top_pointer = 0 : p2_value; -- P2 pushes to val1
    TRUE : val1;
  esac;
  
  next(val2) := case
    p1_pc = 2 & p1_op = 0 & p1_value = 2 & top_pointer = 0 : p1_value; -- P1 pushes to val2
    p2_pc = 2 & p2_op = 0 & p2_value = 2 & top_pointer = 0 : p2_value; -- P2 pushes to val2
    TRUE : val2;
  esac;
  -- update stack on push or pop
  next(top_pointer) := case
    -- Push operations: set top_pointer to the node being pushed
    p1_pc = 2 & p1_op = 0 & p1_value = 1 & top_pointer = 0 : 1;  -- P1 push val1 to empty
    p1_pc = 2 & p1_op = 0 & p1_value = 2 & top_pointer = 0 : 2;  -- P1 push val2 to empty
    p2_pc = 2 & p2_op = 0 & p2_value = 1 & top_pointer = 0 : 1;  -- P2 push val1 to empty
    p2_pc = 2 & p2_op = 0 & p2_value = 2 & top_pointer = 0 : 2;  -- P2 push val2 to empty
    -- Pop operations: set top_pointer to 0 (empty)
    p1_pc = 2 & p1_op = 1 & top_pointer > 0 : 0;  -- P1 pop: empty stack
    p2_pc = 2 & p2_op = 1 & top_pointer > 0 : 0;  -- P2 pop: empty stack
    TRUE : top_pointer;
  esac;
  -- Process 1 return value on pop
  next(p1_ret) := case
    p2_modified : 0; -- if process 2 modified, return empty
    p1_pc = 1 & p1_op = 1 & top_pointer = 1 : val1; -- return val1
    p1_pc = 1 & p1_op = 1 & top_pointer = 2 : val2; -- return val2
    p1_pc = 1 & p1_op = 1 & top_pointer = 0 : 0;    -- return empty
    TRUE : p1_ret;
  esac;
  -- Process 2 return value on pop
  next(p2_ret) := case
    p1_modified : 0; -- if process 1 modified, return empty
    p2_pc = 2 & p2_op = 1 & top_pointer = 1 : val1; -- return val1
    p2_pc = 2 & p2_op = 1 & top_pointer = 2 : val2; -- return val2
    p2_pc = 2 & p2_op = 1 & top_pointer = 0 : 0;    -- return empty
    TRUE : p2_ret;
  esac;
  -- Process 1 push return value
  next(p1_ret_push) := case
    p2_modified : FALSE; -- if process 2 modified, push failed
    p1_pc = 1 & p1_op = 0 & top_pointer = 0 : TRUE;  -- push succeeded (to empty stack)
    p1_pc = 1 & p1_op = 0 & top_pointer > 0 : FALSE; -- push failed (stack not empty)
    TRUE : p1_ret_push;
  esac;
  -- Process 2 push return value
  next(p2_ret_push) := case
    p1_modified : FALSE; -- if process 1 modified, push failed
    p2_pc = 2 & p2_op = 0 & top_pointer = 0 : TRUE;  -- push succeeded (to empty stack)
    p2_pc = 2 & p2_op = 0 & top_pointer > 0 : FALSE; -- push failed (stack not empty)
    TRUE : p2_ret_push;
  esac;
DEFINE
  -- Thread 1 operation predicates
  p1_push_invoked := (p1_op = 0) & (p1_pc = 1);  -- executing push
  p1_pop_invoked  := (p1_op = 1) & (p1_pc = 1);  -- executing pop
  -- Thread 2 operation predicates  
  p2_push_invoked := (p2_op = 0) & (p2_pc = 1 | p2_pc = 2);  -- executing push
  p2_pop_invoked  := (p2_op = 1) & (p2_pc = 1 | p2_pc = 2);  -- executing pop
  -- Combined operation predicates
  invoked_push := (p1_push_invoked | p2_push_invoked);
  invoked_pop  := (p1_pop_invoked | p2_pop_invoked);
  -- Return predicates
  returning_push := (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);
  -- Success return predicates
  push_return := (p1_ret_push) | (p2_ret_push);
  pop_return  := (p1_ret != 0) | (p2_ret != 0);
The HyperLTL formula(s)¶
Linearizability compares the operation histories of a concurrent execution with those of a sequential witness. If every step of the histories aligns, the concurrent execution is considered correct; any divergence reveals a violation.
Forall A . Exists B .
G((invoked_pop[A]=invoked_pop[B])&(returning_pop[A]=returning_pop[B])&(pop_return[A]=pop_return[B])&
(invoked_push[A]=invoked_push[B])&(returning_push[A]=returning_push[B])&(push_return[A]=push_return[B]))