Abstract 1 Introduction 2 Motivating Example 3 Programming Language and Specifications 4 Linearizability and Possibilities 5 Program Logic 6 Related Work 7 Evaluation and Conclusion References

A Complete Program Logic for Compositional Linearizability

Eashan Hatti ORCID Yale University, New Haven, CT, USA    Arthur Oliveira Vale ORCID Yale University, New Haven, CT, USA    Zhongye Wang ORCID Yale University, New Haven, CT, USA    Yueyang Feng ORCID Yale University, New Haven, CT, USA    Zhong Shao ORCID Yale University, New Haven, CT, USA
Abstract

We present Linearizability Hoare Logic (LHL), the first mechanized, sound, and complete program logic for atomic, set, and interval linearizability. We achieve this by showing soundness and completeness of LHL w.r.t. a more general criterion, compositional linearizability, which subsumes all three criteria. We showcase the expressivity of LHL by verifying an exchanger with a set linearizable specification, the elimination-backoff stack built above the exchanger, a lock with an atomic linearized specification, and a write-snapshot object with an interval linearizable specification.

Together with LHL we formalize a modular verification framework for concurrent components based on the theory of compositional linearizability. This allows us to specify components at a high level of abstraction and granularity, and then assemble them into large systems that are correct by construction. As a showcase, we verify the elimination-backoff stack modularly by verifying each of its sub-components against their linearized specifications and then linking them together.

Keywords and phrases:
Program Logic, Rely-Guarantee, Linearizability, Compositional Verification, Concurrency
Copyright and License:
[Uncaptioned image] © Eashan Hatti, Arthur Oliveira Vale, Zhongye Wang, Yueyang Feng,
and Zhong Shao; licensed under Creative Commons License CC-BY 4.0
2012 ACM Subject Classification:
Theory of computation Program verification
; Computing methodologies Concurrent computing methodologies ; Theory of computation Programming logic
Related Version:
Extended Version: https://flint.cs.yale.edu/publications/lhl.html [14]
Acknowledgements:
We would like to thank the anonymous reviewers and artifact evaluation committee for their thoughtful feedback and time spent reviewing our work.
Funding:
This work is supported in part by NSF grants 2019285, 2313433, 2442888, and by the Defense Advanced Research Projects Agency (DARPA) under Agreement No. HR00112590130. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the authors and do not necessarily reflect the views of the funding agencies.
Supplementary Material:
Software  (ECOOP 2026 Artifact Evaluation approved artifact): https://doi.org/10.4230/DARTS.12.1.5
Editors:
Robbert Krebbers and Alexandra Silva

1 Introduction

1.1 Compositionality

When designing large computer systems, it is good practice to encapsulate independent functionalities in separate components. This benefits verification, as a well-organized system decomposes into small open components that are completely characterized by their own implementation and their interfaces with the rest of the system.

In this context, given a module M that implements some object with interface F by importing a library with interface E, we call the implemented object the overlay of the module and the imported library its underlay. To verify the module against its overlay specification, we only consider the relevant underlay specifications, allowing provers to write independent proofs for separate modules. This means that the internals of the underlay module do not matter for other parts of the system: the module is characterized by its underlay and overlay specifications. This mode of verification is only possible in a compositional verification framework, where each module can be given a correctness property independent of the rest of the system, and then can be used as a building block to construct a larger system, with no side-conditions for the correctness of their compositions.

In the context of concurrent objects, linearizability [16] has been the gold standard since the 90s. It allows programmers to abstract the behavior of a concurrent object E’s code M_E into a simpler specification V_E that is atomic111By atomic we mean that when a thread makes an invocation it receives its response immediately after, with no interference from other threads. and easier to reason about. While there are many efforts to enable the mechanized verification of linearizability, few are truly scalable: either there are concurrent objects like the exchanger with non-atomic specifications that cannot be expressed in these pre-existing frameworks due to their over-reliance on atomicity, or the specification cannot be used as a black box encapsulating its code to modularly verify another object that uses this underlay object.

1.2 Compositional Linearizability

Over time, linearizability, which was an inherently atomic correctness criterion, has been extended to handle non-atomic objects [25, 5]. Recently, [27, 26] have also presented an analysis of compositional models of computation which reveals that linearizability, even when fully generalized to handle non-atomic blocking concurrent objects, has an underlying compositional structure, which can be exploited to develop a compositional refinement calculus for the verification of concurrent modules. As an application of their theory, [27] develop an axiomatic technique for showing linearizability of individual traces, and then package it as a rely-guarantee program logic. They call their linearizability criterion and underlying framework compositional linearizability.

While an important first step towards the compositional verification of non-atomic concurrent objects, and a convincing showcase of the compositional linearizability theory, the verification technique proposed by [27] could still be improved for the sake of practicality in large systems verification. Some immediate issues are that their framework has not been validated by a mechanization, and while [27] give a paper proof of soundness, they only conjecture its completeness. In this paper, we set out to mechanize the verification technique and formalize the soundness proof of [27], while also proving its completeness. Completeness is the main contribution of our work, and results in the first complete program logic for any of the standard generalizations of linearizability beyond atomicity.

In doing so, we identified an additional issue for the sake of practicality. To exploit the trace-based theory they had developed earlier, [27] use traces as a universal notion of state. Unfortunately, in practice, using traces as state leads to unnecessary complexity in defining predicates, as simple properties that would be readily available in a concrete representation of state become properties that must be stated inductively over the underlying trace.

So, in order to enable us to verify complex examples, we found it paramount to switch to a state-based representation of proof configurations. It turns out that switching from trace-based reasoning to state-based reasoning is not a simple task. There are two aspects to this. First, [27] use a technique inspired by [16] which they call possibility reasoning, where one attempts to maintain a set of possible linearizations for the current computation through a representation of linearizations in a structure they call a possibility. Unfortunately, some of the possibility update rules of [27] do not readily generalize to a state-based formulation, as they involve manipulating the ordering of events on a trace.

Another aspect of switching to state-based reasoning is that many of the algebraic properties [27] rely on to show the compositionality of the verification framework no longer hold strictly. Technically, their trace-based representation allows them to work up to equality to show several categorical properties of their refinement calculus. We had to generalize these categorical properties to hold only up to notions of equivalence in order to generalize their results to concrete states. In the process, we end up mechanizing a large portion of a generalization of the theory in [27].

Summary and Main Contributions
  • We present the first mechanized program logic, Linearizability Hoare Logic (LHL), which supports and unifies the verification of Herlihy-Wing linearizability and other non-atomic linearizabilities into a generalized criterion called compositional linearizability [27].

  • To mechanize compositional linearizability, we first formalize a compositional model of computation in § 3, formalizing modules, specifications, objects, refinement and composition operations, and proving their algebraic and compositional properties.

  • In § 4, we generalize compositional linearizability, originally based solely on traces, to our LTS setting, along with its compositional properties.

  • Finally, we present proof rules of our LHL in § 5, which uses a technique called possibility reasoning to construct linearizability proofs. We prove its soundness and completeness w.r.t. compositional linearizability.

  • Using LHL, we prove several representative examples, including: a modular proof of the elimination-backoff stack [15] in the structure proposed in Fig. 1, to be explained throughout the paper; we mechanize a lock and then a coarse-grained locked racy object (the motivating example used by [27]); to showcase full-blown non-atomicity, we also mechanize an interval-linearizable [5] write-snapshot object.

All results of the paper are mechanized in Rocq and are found as supplemental material. In addition, an extended version [14] of this paper is available containing further details on the elimination-backoff stack and a detailed comparison between our paper and [27].

2 Motivating Example

Throughout the paper, we use the elimination-backoff stack [15] (henceforth “EBStack”) to demonstrate our approach.

The EBStack can be decomposed into multiple concurrent objects as shown in Fig. 1. Each object implements a specific overlay specification defined as a labelled transition system (LTS), and only uses operations provided in its underlay interfaces. For example, the top-level EBStack object implements the usual stack specification V_EBStack above the exchanger object and a try stack (a stack whose operations may fail). As shown in the code in the right box, the EBStack will first try to perform a push/pop operation through the try stack. If the operation fails, the EBStack tries to eliminate with another operation through the exch operation of the exchanger object, which only succeeds if there are concurrent exch operations. The EBStack’s push/pop operations loop until one of these two steps succeeds. The exchanger implementation is encapsulated in a different module Exch, which is built above the CAS memory cells. To increase throughput, an array of exchangers is used instead of the single exchanger in the EBStack [15]. In § 3, we explain in detail the programming language we use to implement these objects and LTS for specifying objects.

Figure 1: The exchanger implementation (left), and the hierarchy (middle) of underlay object implementations (M_xxx) and their interfaces (V_xxx) used in the elimination-backoff stack implementation (right): A is the type of values stored in the stack; Offer is a data type defined by Offer:=Off(α,v)|Acc(α,β,v,w), where α,β𝒯 are thread identifiers and v,wA; The value indicates the failure of the operation in both the exchanger and the EBStack object.

The key benefit of this layered approach is that, with proper encapsulation, the implementation and verification of each object become relatively concise, manageable, and, most importantly, independent of other objects. In the hierarchy in Fig. 1, the EBStack code depends only on the V_Exch and V_TryStack interfaces and is decoupled from the implementation and verification of these overlay objects. After verifying the EBStack layer, its proof will remain untouched when the implementation of its underlay objects is modified, as they are all verified to obey the object specifications that are independent of the implementation code. For example, the exchanger array ExchArr and the single exchanger Exch share the same overlay specification V_Exch because they are meant to implement the same operation with the same functionality, and, as a result, we may use either one as the V_Exch library in the overlay EBStack object without any modification to any implementation code or proof.

This leads to the reason that we are interested in the verification of the elimination-backoff stack. The variations of EBStack shown in Fig. 1 have been verified in other concurrent object verification frameworks [23, 11]. However, none of them achieves the same degree of compositionality shown in this diagram. They all choose to inline the code of the exchanger into the code of the EBStack, which results in complicated EBStack code and proof. Some choose to inline the single exchanger code, which brings another problem: when they want to replace the single exchanger with a more performant implementation like the exchanger array, they need to redo the entire proof of the EBStack. We further discuss the comparison between their proofs and ours in § 6.

The reason for this is that their specifications and correctness criteria cannot support non-atomic objects, such as the exchanger object. An exchange only makes sense if two threads are executing concurrently, which is not possible in an atomic specification. Therefore, the exchanger object does not admit a deterministic atomic linearized specification. Hence, it cannot be verified as a separate module with strict proof encapsulation, and its proof has to be merged into the proof of the overlay stack object. We use the theory of compositional linearizability to specify the exchanger by a set-linearizable LTS, and use a program logic based on possibility reasoning to verify the exchanger and other objects as standalone modules. Compositionality ensures the correctness of the top-level stack w.r.t. its specification V_EBStack in a system running above the hardware interfaces, given the per-module correctness w.r.t. their own overlay specs. We elaborate on this verification technique in § 4 and § 5.

3 Programming Language and Specifications

Our framework verifies concurrent modules M:Mod E F, which implement operations specified by an effect signature F. A module M implements each operation fF as a program Mf : Prog Ear(f), which uses operations from an underlay signature E and returns a value in the return type (specified by ar(f)) of the operation f that it implements. An object over an effect signature E is specified by a state-transition system V_E : Spec E. The core verification task is to show that given a specification V_E : Spec E, called the underlay of M, the module M:Mod E F correctly implements its overlay specification V_F : Spec F. We now discuss these notions formally.

3.1 Effect Signatures

An effect signature consists of a set E of effects e and two assignments parE():ESet and arE():ESet, specifying respectively a set of parameters and a set of return values for each effect eE. We find it convenient to encapsulate this information in the following notation, whose usage will become clear shortly:

E:={e:parE(e)arE(e)eE}

When it causes no confusion, we omit the subscript on parE() and arE().

In the context of our paper, effects eE are method names, which take as arguments elements of the parameter set par(e) and return some value in its arity ar(e). For instance, the signature for the EBStack data structure is the following, where A is the type of stack elements and 𝟏 is the unit type.

EBStack:={push:A𝟏,pop:𝟏A+𝟏}

Signatures also support a union operation, which collects the operations of both signatures into a single, combined signature. Formally:

E+F:={inl e:parE(e)arE(e)eE}{inr e:parF(e)arF(e)eF}

3.2 Programs

Programs play the role of method bodies within modules in our mechanization. We formalize programs as interaction trees [34], which we type as Prog. Interaction trees are coinductively defined data structures with the following syntax:
pProg ER:=Visf(a)kRetrTaup where fEapar(f)k:ar(f)Prog ERrR

A program pProg ER represents a (possibly infinite) series of operations that result in a return value of type R (if finite). The three constructors function as follows.

Vis f(a) k:

Issues an invocation to the operation f(a) of the underlay signature E, and proceeds with the continuation k(v) after receiving a return value var(f).

Ret r:

Once a program executes all of its operations, it terminates by returning a value r of type R.

Tau p:

Represents a silent step and is necessary for looping constructs to satisfy Rocq’s productivity checker. We refer readers to [34] for more details.

Prog E _ defines a monad for any E, which allows for programs to be written in a monadic style. The program bodies in Fig. 1 are the sugared version of a monadic expression over Prog E _. The additional control structures, such as the if conditional and the while loop constructs used in Fig. 1, may be defined in a standard way.

3.3 Modules

A module M:ModEF implements operations of a signature F, using the operations of the signature E. A module M:ModEF consists of a mapping from operations fF and arguments apar(f) to programs Mf(a):Prog Ear(f), implementing a call f(a) as a program operating over E and returning a value in its return type ar(f). In other words, a module M:ModEF is a collection M:ΠfF(par(f)Prog Ear(f)). For instance, MEBStack is the mapping taking push(v) to the program described in Fig. 1 and similarly for pop(). Nonetheless, we write Mf(a):Prog Ear(f) for the program corresponding to fF and apar(f) in M, and Mf:par(f)Prog Ear(f).

Modules may be composed vertically by interaction tree substitution. Given modules M:ModEF and N:ModFG, their vertical composition M:N:ModEG implements the operation g(a) of G by running the corresponding program Ng(a) with method invocations f(a) to F replaced by their corresponding programs Mf(a). We refer readers to the mechanization for the exact definition. Crucially, the vertical composition operation admits an identity element:

idME :ModEE:=(idProge)eE where idProge:=λ(a:par(e)).Vise(a)Ret

The vertical composition operation (:) and its identity element satisfy the following properties:

(1)idM:MM(2)M:idMM(3)(M1:M2):M3M1:(M2:M3)

where is an equivalence relation defined on modules as the pointwise equivalence of programs up to removal of silent steps (Tau). The first two properties show that idM is the neutral element for vertical composition, up to equivalence, and the third property is its associativity.

3.4 Specifications

We now give a formal definition of labelled transition system (LTS), the specifications we use for objects. Recall that a module M:ModEF depends on the operations provided by E to implement the operations of F. To define the operational semantics of a module, we first define the behavior of the operations in E using an LTS.

First, we must define what kinds of events we are interested in. We assume as given a set of thread names 𝒯, which is a parameter over the whole model. We use a set of the form {ii<T}, where T is the total number of threads, as 𝒯. In a multicore setting, 𝒯 is the set of cores available; in a multi-threaded setting, it is the set of available threads. While users can fix the whole set 𝒯 when verifying an object, we choose to parameterize our theories and proofs over 𝒯. As a result, our proofs of linearizability universally quantify over such finite sets of thread names.

Then, an event of the signature E consists of

TEventE:=𝜶:f(a)𝜶:f(a).v

where α𝒯, fE, apar(f) and var(f). An event is either 𝜶:f(a), a call to an operation of E, or 𝜶:f(a).v, a return event tagged with the thread that issued it. Then, a labelled transition system V over E, written V:SpecE, is defined as a triple (S,V,s0) of a set of states S, a transition relation VS×TEventE×S, and an initial state s0S.

Unlike many approaches for modeling concurrent specifications, which assume operations are logically atomic, the call and return events are separate transitions in our specifications. This is necessary to model objects that are not linearizable to atomic specifications. We do, however, require that the labelled transition system be locally sequential, i.e., the projection to each thread is sequential (it strictly alternates between invocations and responses). As a result, we must carry some extra state, like the operation that is currently executing (e.g., Fig. 2(a)), so we prevent other calls from happening until the current pending call is completed.

We now give a couple of examples of linearized state transition systems used in the verification of the EBStack. In our state diagrams, such as Fig. 2(a), we use conditionals and state updates in the labels, and depict the transitions for abstracted thread names α, β, to mean that for any thread α a certain transition exists (if threads have different names they are distinct αβ). This allows us to make the diagrams finite so they may be depicted succinctly on paper.

(a) CAS cell specification V_CAS. The CAS cell has two operations: get() for retrieving the stored value v, and cas(e, n) for performing the compare-and-swap operation.
(b) Exchanger specification V_Exch. The exchanger operation exch allows two threads to attempt to exchange values with other threads.
Figure 2: CAS and Exchanger Linearized Specifications.
Example 1 (Atomic CAS Cell LTS).

We begin with an atomic specification encoded by the LTS in Fig. 2(a) for a CAS cell object. Its states are tuples consisting of the stored value and the ongoing pending call. A non-empty pending call component restricts the next transition to be labelled with the matching return event. The two transitions on the left ensure that a get operation 𝛂:get() always gets the stored value v in the return event 𝛂:get().v. The three transitions on the right specify that a CAS operation 𝛂:cas(e,n) receives true as a response only when the stored value is e and stores the new value n to it, and otherwise, it receives false and does not modify the stored value. This LTS specifies an atomic object because, after an invocation by thread α, the next transition must be a response by the same thread α. Invocation transitions are possible only when the ongoing operation is , resulting in only sequential executions.

Example 2 (Set-Linearizable Exchanger LTS).

Fig. 2(b) defines the LTS specification for the exchanger object. Its LTS state consists of two sets over tuples of a thread name and an exchanged value. The left and right sets are the “offer set” and “accept set”, containing respectively the exchange offers that threads have proposed and accepted. In a successful exchange, two threads will add their offers to the offer set, after which one thread accepts the other offer. The remaining thread will then accept another offer and empty the set. Tracing through the blue arrows, we can extract the execution of this successful exchange as trace (1).

𝜶:exch(v)𝜷:exch(w)𝜶:exch(v).w𝜷:exch(w).v (1)

In a failed exchange, the offering thread will revoke its offer and return . This will step through the dotted blue arrow and the red arrow as trace (2).

𝜶:exch(v)𝜶:exch(v). (2)

All possible executions of the exchanger in the specification are repetitions of trace (1) and trace (2). In trace (1), calls are not necessarily followed immediately by their returns – the calls and returns of both threads α and β are interleaved with each other. As a result, this specification is not atomic linearizable but is set linearizable [25]: calls and returns in the exchanger’s execution traces always come as a set of at most two calls followed by a set of matching returns.

3.5 Traces and Refinement

We define a trace over a signature E as a list t(TEventE) of thread events. We write TraceE for the set of traces over E. The LTS specification V:SpecE specifies the behavior of an object through V, a set of traces defined as follows: we first define a path in V=(S,V,s0) and then take all paths starting from the initial state as the set of behaviors.

Our notion of refinement is then just behavior containment:

It is straightforward to check that refinement is reflexive and transitive, making it a preorder.

3.6 Operational Semantics

Given an underlay specification V_E:SpecE, where V_E=(S,V,s0), and a module M:ModEF, we define the behaviors of M running on top of V_E operationally as a concrete LTS (V_EM). In § 4, we define the linearizable relation of the concrete LTS w.r.t. the specification LTS (those in Fig. 2).

The operational semantics models executions under an arbitrary scheduler and client to the overlay object F. An arbitrary client may have a thread call any operation available to it, with any argument. Hence, each operational semantics rule non-deterministically chooses a thread to take a step. This ensures we verify our modules as open components.

The states (ths,s)InterState (interaction state) of the operational semantics are pairs of a thread state ths𝒯TStateEF, which is a map from thread names to the continuation of that thread, and an underlay LTS state sS. The continuation of each thread is defined as

TStateEF:=IdleContf(a)pUCallf(a)g(b)k

where fF, apar(f), pProg Ear(f), gE, bpar(g), k:ar(g)Prog Ear(f). We use ths[αp] as the notation for updating the value of α to p in the map ths.

Figure 3: Operational Semantics.

Fig. 3 defines the operational semantics rules. OCall selects an idle thread, non-deterministically chooses an operation fF and an argument apar(f), and changes the continuation for α to the code for f(a) defined as Mf(a). UCall locates a thread α whose next step in the continuation is an underlay call g(b), performs the call step 𝜶:g(b) in the underlay object V_E, and updates the underlay LTS state to s accordingly. URet is analogous, except that it takes a return transition and continues with the continuation as specified by k. ORet matches on a thread whose continuation indicates it is ready to return from the overlay operation, and performs the overlay return by updating the continuation to Idle. Silent simply skips a Tau with no change to the state.

We then define the concrete LTS of running M on top of V_E as V_EM:SpecF:=(InterStateFV_E,V_EM,(ths0,s0)), where ths0 is the constant mapping to Idle and mTEventF, and transitions are given by the following relation

(ths,s)V_EMm(ths,s) ths′′𝒯TStateEF,s′′S,t(TEventE).
(ths,s)m(ths′′,s′′)(ths′′,s′′)t(ths,s)

Notice that only the overlay events mTEventF are visible in V_EM. The definition allows for a visible step m followed by several steps in the underlay object V_E, as enforced by the requirement that t(TEventE). The underlay event trace is hidden behind an existential quantifier, making these implicit silent steps.

The following are some important properties of the operation - -. The first two properties are monotonicity of vertical composition w.r.t. and , while the third property encodes both associativity of and :, as well as compatibility of the two operations.

3.7 Horizontal Composition

Both modules and specifications can be horizontally composed. For instance, given modules ML:ModELFL and MR:ModERFR we can define their horizontal composition ML+MR:Mod(EL+ER)(FL+FR), similarly to how we did with effect signatures, as the union of the two collections:

(M_L+M_R)f:={mapProg(λx.inlx)MLff=inlfmapProg(λx.inrx)MRff=inrf

where mapProg(λx.inlx) replaces every event Vise in MLf by Vis(inle) (and similarly for the other branch).

For specifications V_L:SpecEL and V_R:SpecER we can define a specification V_LV_R given by the (locally sequential) asynchronous product of the two transition systems. We refer the reader to the mechanization for the definition. Below, we show the key compositional properties of :

The first three properties show that horizontal composition defines an order-isomorphism for specification refinement. The last two properties show that the operation is functorial.

4 Linearizability and Possibilities

We now define the notion of linearizability used in the paper (§ 4.2), which is based on the compositional linearizability formulation by [27]. Then, we go over how we implement one of the main techniques for proving linearizability: possibility reasoning (§ 4.4).

4.1 Background

Safety for sequential objects is usually given by functional correctness. For instance, a counter object provides operations get:𝟏, taking unit as argument and returning some natural number, and inc:𝟏𝟏, taking unit as argument and returning unit. An implementation of a counter is correct as long as get always returns the number of previous inc operations. This means that given the trace:

s=incokget

of a correct counter implementation, we are guaranteed that the next event is a return of 1 to the operation get. However, for a concurrent counter implementation, given the trace:

α1:incα1:okα1:get

any number of increment operations by other threads might take effect:

α1:incα1:okα1:getα2:incα2:okαk:incαk:okα1:n

so that n might be any number between 1 and k. What makes a concurrent counter object correct is therefore a more challenging question, and expressing “functional” correctness for concurrent objects remained an open question until the 90s, when linearizability [16] was proposed.

Linearizability asks that every trace generated by the concurrent counter implementation, such as trace s above, be linearizable with respect to some trace of a correct sequential counter. This is formalized by defining a partial order called happens-before, which associates to a trace s the partial order 𝗁𝖻(s) defined as the smallest partial order such that e𝗁𝖻(s)e whenever e and e are events by the same thread and e appears before e in s, or when e is a return event and e is an invocation event. A trace s is then said to be linearizable w.r.t. an atomic trace t when there is a way to complete some pending invocations of s (by adding extra return events), then remove all remaining pending invocations, so as to obtain a trace s satisfying that 𝗁𝖻(s) is refined by 𝗁𝖻(t) (in the sense that if e𝗁𝖻(s)e then e𝗁𝖻(t)e). Atomic means that every invocation is immediately followed by its return, or, equivalently, that t is locally sequential (every thread alternates invocations and returns) and 𝗁𝖻(t) is a linear order. An implementation is then said to be linearizable w.r.t. a specification if all traces generated by the implementation are linearizable w.r.t. the specification.

The requirement that t, and consequently specifications, be atomic ensures that concurrent data structures such as stacks, queues, and counters are all specified by essentially the same specification as the corresponding sequential data structures. This requirement, however, proves too restrictive, as many reasonable concurrent objects admit no corresponding sequential object. For example, the exchanger in § 2 is a standard synchronization primitive (for instance, available as part of Java’s standard libraries [4]). Nonetheless, it admits no reasonable atomic specification, intuitively because an exchange can only happen if two threads are running concurrently. To remedy this, set linearizability was proposed [25]. Set linearizability only modifies the definition of linearizability by requiring that the traces of the specification be of the form:

I1R1I2R2InRn

where each Ik is a sequence of invocations and each Rk is a sequence of responses containing all and only the responses to the invocations in Ik (except for Rn, which may contain only some such responses). In addition, one requires that if

I1R1I2R2InRn

is in the specification, then so is

I1R1I2R2InRn

where each Ik (resp. Rk) is some reordering of Ik (resp. Rk). This means that we may see these traces as sequences of sets of invocations, each followed by the set of returns to that set of invocations.

This modification to linearizability allows it to specify many concurrent objects whose behaviors involve some kind of synchronization, such as exchangers, barriers, and blocking stacks (stacks where pop blocks on the empty stack). We will see that it also helps in specifying objects beyond synchronization primitives, such as the TryStack. However, there are concurrent objects and certain distributed tasks whose consistency is even weaker than that provided by set linearizability. For example, it is possible to implement a write-snapshot object [3]222A write-snapshot provides a single operation ws that takes a value as argument and returns a set of written values as output. validating the trace:

s=𝜶:ws(3)𝜷:ws(5)𝜶:ws(3).{3,5}𝜸:ws(2)𝜷:ws(5).{3,5,2}

Here, when α takes its snapshot, β’s value of 5 is already visible, but by the time β manages to take its own snapshot, γ’s value is now visible too. Because of this, the return to β’s call cannot happen together with α’s return, and hence set linearizability cannot capture this object’s consistency.

To accommodate such objects, interval linearizability [5] was proposed, which further relaxes the notion of specification in linearizability’s definition so that in a trace

I1R1I2R2InRn

Rk is now allowed to contain returns to pending invocations from Il for any lk, and in particular it may contain only some of the returns in any Il (including Ik). The requirement that reorderings within Ik or Rk are all valid remains. This has been shown to be a complete correctness criterion for distributed tasks [5, 12]. Interval linearized specifications therefore allow traces with a significant degree of concurrency, such as s above. Nonetheless, these specifications may still be simpler than the traces generated by the implementation. The write-snapshot we discuss and verify in our mechanization admits an interval linearized specification that is deterministic in the sense that given a trace:

I1R1Ik

then in any two extensions

I1R1IkRkandI1R1IkRk

where Rk and Rk contain only responses, if 𝜶:ws(v).S appears in Rk then either 𝜶:ws(v).S also appears in Rk or Rk can be extended with 𝜶:ws(v).S. In other words, any returns that can happen for any given thread are unique up to the ordering of the responses. This makes the linearized specification significantly easier to work with for a client of the write-snapshot object than using all the traces generated by the implementation, which present more concurrency and non-determinism.

Nonetheless, interval linearizability still does not accommodate all concurrent objects. This last issue is more subtle, and we recommend that the interested reader consult [27] for a more thorough treatment. Succinctly, the requirement that all pending invocations that have not been completed must be removed in the linearization is too strong for certain concurrent objects. There are circumstances where an invocation is detectable to other threads even though its response is still not enabled. Compositional linearizability [26, 27] further weakens interval linearizability by allowing some invocations to be completed, some to be removed, and some to remain in the linearization. This makes compositional linearizability complete for concurrent objects. However, perhaps the main advance of compositional linearizability is that it gives a novel treatment of the theory of linearizability showing that, within a compositional model of computation, linearizability can be characterized entirely in terms of composition and refinement. In this paper, we adopt compositional linearizability as our notion of correctness and mechanize many aspects of the comprehensive theory of [27]. In particular, they present rather algebraic proofs of all the key properties of linearizability based on a different (but equivalent) formulation that circumvents the use of partial orders or rewrite systems. We have found that this reformulation makes mechanization tractable despite the significantly more challenging setting of working with non-atomic and potentially blocking specifications. At the same time, we find it productive to formulate their ideas using LTSs rather than strategies (final coalgebras for these LTSs). To do so, we made several improvements to their proof technique based on possibilities. The remainder of this section concerns our mechanization of compositional linearizability and possibilities, ultimately leading to our proof technique in § 5.

4.2 Linearizability

The observation at the core of [27] is that, in the context of a compositional model for computation, the linearizability theory can be entirely derived from the definition of the identity for composition of modules. They refer to their notion of linearizability as compositional linearizability and demonstrate that it is a conservative generalization of Herlihy-Wing linearizability [16], set linearizability [25], and interval linearizability [5], as it becomes equivalent to these criteria under appropriate restrictions.

In our mechanization, we choose to formalize compositional linearizability using its refinement-based definition, which can be defined using idM as follows:

Definition 3.

Given a specification V:SpecE, we define KV:SpecE by KV:=VidM.

We say a concrete LTS V:SpecE is linearizable w.r.t. V:SpecE, written VV when VKV.

Intuitively, the reason why this definition agrees with the usual linearizability definition is that the set of behaviors of KV is exactly the set of all traces linearizable w.r.t. some behavior of V. The operations of E made by the overlay to idM form a bracket around the corresponding underlay operations. Because of this, happens-before ordering is preserved from the overlay of idM to its underlay. The fact that pending calls may be removed or completed in a trace corresponds to the fact that in the operational semantics of VidM, one can be in a state where the call has been made in the overlay of idM but not in the underlay yet. There are states where a return has been made to the underlay operation in idM but has not returned to the overlay yet. Therefore, KV defined above contains all traces linearizable w.r.t. the specification. We refer readers to [27] for a detailed treatment of how this definition relates to previous definitions of linearizability.

Our definition of linearizability enjoys observational refinement and locality (Theorem 4). They follow from generalizing the proofs of compositional linearizability from [27] to handle our more heterogeneous compositional structure, discussed in § 3.

Theorem 4 (Observational Refinement and Locality).

For any V_E,V_ESpecE, V_F,V_FSpecF, and MModEG, we have observational refinement (1) and locality (2):

  1. (1)

    Behaviors of running any module M above the concrete LTS V_E are contained in those of running the same M above the linearized specification V_E, i.e., V_EV_E(V_EM)(V_EM).

  2. (2)

    A system of multiple objects is linearizable iff all of these objects are separately linearizable, i.e., V_EV_EV_FV_FV_EV_FV_EV_F.

4.3 Compositionality

Through compositional linearizability [27], LHL gains horizontal and vertical composition without any additions to the logic. LHL’s soundness provides linearizability proofs of different modules, which can be horizontally and vertically composed together to obtain the linearizability of a larger system. These results are obtained by combining locality and observational refinement with the compositional structure of the model defined in § 3.

Theorem 5 (Horizontal Comp.).

If (V_E1M_F1)V_F1 and (V_E2M_F2)V_F2, then ((V_E1V_E2)(M_F1M_F2))(V_F1V_F2).

Theorem 6 (Vertical Comp.).

If (V_EM_E)V_F and (V_FM_F)V_G, then (V_E(M_E:M_F))V_G.

Horizontal composition enables importing multiple objects together through the tensor operator () and using them concurrently. The locality of compositional linearizability guarantees that both objects will preserve their original specifications, and users of both objects can enjoy the interface F1+F2 with both sets of operations available.

Vertical composition () “links” the overlay object’s code with the imported underlay object’s code. Since vertical composition () is compatible with linking (:), the observational refinement [27] ensures running the linked code M_E:M_F above the lower-level specification V_E still produces traces linearizable to the top-level specification V_F.

This is how we “glue” together the individual verified components in Fig. 1 into the whole implementation of the EBStack to obtain the correctness of the entire system.

4.4 Possibility Reasoning

To prove linearizability, we employ possibility reasoning. [27] have shown a bisimulation between possibilities and their formulation of the operational semantics of idM running on top of V_F, implying that possibility reasoning is sound and complete for compositional linearizability proofs. In our setting, we redesign the possibility reasoning technique to contend with our use of state as explained in § 1.

Assuming we want to show M running above V_E is linearizable w.r.t. V_F, i.e., (V_EM)V_F, a possibility ρPoss is defined as a triple ρS,ρC,ρR of (1) the linearized state ρS in V_F, (2) a partial map ρC from thread names α𝒯 to call events of F, and (3) a partial map ρR from thread names α𝒯 to return events of F. We write ρC(α) for the event that ρC maps α to; ρC(α)= when it is undefined; and ρC[αm] for substitution of m for ρC(α); and similarly for ρR. The mapping ρC stores pending calls that have happened in the concrete execution but have not been performed in the linearized state s; ρR stores returns that have been made in the linearized state ρS, but not in the concrete execution. Therefore, from the perspective of a particular thread α, a possibility can be in one of four states:

Idle:

The last transition by α in V_F was a return, and both ρC(α)= and ρR(α)=.

Pending Call:

α’s last transition in V_F was a return, ρC(α)=m for some mF and ρR(α)=.

Call Done:

The last transition by α in V_F was a call 𝜶:m, ρC(α)=m and ρR(α)=.

Pending Return:

The last transition by α in V_F was a return 𝜶:m.n, ρC(α)=m and ρR(α)=m.n.

We formalize possibility updates, the axioms to manipulate possibilities, as a transition system whose states are possibilities over V_F and transitions are given by:

Figure 4: Possibility update rules.

Intuitively, a possibility encodes a proof of linearizability. The Invoke and Return model the events of the concrete trace, and CCommit and RCommit the corresponding events in the linearization. The axioms on possibility updates ensure that as long as the initial possibility is valid, the updated possibility always corresponds to some valid linearization, i.e., (1) only call events that happened in the concrete execution can be added to the possibility, (2) only transitions respecting local sequentiality can be made to the possibility, and (3) return events missing in the concrete execution can be added to complete pending calls in the possibility. The remaining obligation to show the consistency of linearized return values and actual returned values will be addressed by the program logic. For instance, a possible proof that the trace t below is linearizable w.r.t. ti,j corresponds to the sequence of possibility updates shown in Fig. 5.

t =𝜶𝟏:push(1)𝜶𝟐:push(2)𝜶𝟏:push(1).()𝜶𝟐:push(2).()𝜶𝟑:pop()
ti,j =𝜶𝒊:push(i)𝜶𝒊:push(i).()𝜶𝒋:push(j)𝜶𝒋:push(j).()𝜶𝟑:pop()𝜶𝟑:pop().j

When verifying a stack implementation, we can determine if the correct linearization is t1,2 or t2,1 by inspecting the underlay state. If we focus on Invoke and Return events, we can reconstruct the concrete trace t, and if we focus on CCommit and RCommit, we can rebuild the linearized trace ti,j.

Figure 5: Example of possibility-based linearizability proof.

Single possibilities are not sufficient in general when constructing proofs of future-dependent linearizations inductively. We must maintain multiple possibilities simultaneously to contend with current possibilities being invalidated in the future. To accomplish this, we lift the possibility axioms from a single possibility ρ to a non-empty set of possibilities ρ¯, and define the lifted transition .333Formally, we have described how to construct the equivalent of forward simulations. Using a power-set construction, we obtain the equivalent of a forward-backward simulation, which is complete for trace equivalence [24]. In other words, a set of possibilities ρ¯ can be updated into σ¯ as long as every possibility in σ¯ is reachable after zero or finitely many valid updates from some possibility in ρ¯.

In the stack example above, if the stack’s linearizability proof were future-dependent (e.g., the timestamped stack [8]), we would maintain a set ρ¯={ρ1,2,ρ2,1} for the trace t. The possibility ρ1,2 corresponds to the case i=1 and j=2, and ρ2,1 the other case. We would then update ρ¯ according to the updates in the example above for the corresponding assignments of i and j. When the pop() operation reaches its linearization point, we would find out which of the two pushes was deemed to have occurred first, and remove whichever of ρ1,2 or ρ2,1 has been invalidated.

5 Program Logic

Now we are ready to present our program logic (§ 5.15.6) and its soundness and completeness (§ 5.7). Our program logic, building on that of [27], is a Rely-Guarantee program logic. The main idea of rely-guarantee reasoning [18] is that each thread over-approximates its local steps in terms of a guarantee relation 𝒢, and over-approximates its environment steps (steps by other threads) in terms of a rely relation . One then shows that the thread guarantees its steps follow 𝒢, while relying on its environment following . The main aspect of rely-guarantee reasoning is the notion of stability, which is what enables parallel composition to become a congruence.

Stability is the notion that when a thread shows that steps satisfy a predicate P, such as pre-conditions, invariants or post-conditions, these predicates must be shown to be stable with respect to , in the sense that:

that is, predicates are invariant upon the environment taking steps before or after the predicates (where ; stands for relational composition). When a predicate satisfies this, we say it is stable w.r.t. , written Stable(,P). Stability ensures that any predicates we verify a thread’s local steps to satisfy will not be invalidated by having the scheduler introduce steps by other threads between local steps, and it is fundamental to ensuring threads may be appropriately parallel composed later.

Then, if a thread α is verified against [α] and 𝒢[α], and β is verified against [β] and 𝒢[β], we are allowed to parallel compose them when

which ensures that α and β follow each other’s assumptions about their environments.

5.1 Proof State and Assertions

In LHL, a proof configuration is a tuple (s,ρ¯) of a concrete LTS state sInterState and a set of possibilities ρ¯𝒫(Poss). The component s represents the current state of the program, which follows the actual semantics of the program, while ρ¯ represents linearized overlay LTS states, which are managed by the prover following the possibility update rules in § 4.2.

By using a set of possibilities instead of a single possibility, users of LHL can track different linearizations corresponding to the current state. They can remove extra possibilities later when future events invalidate these linearizations. This allows us to support objects (such as the Herlihy-Wing queue [16]) with future-dependent linearizations [32], which is essential for the completeness proof of LHL.

We define preconditions P as predicates over the proof state, while rely/guarantee conditions ,𝒢 and postconditions Q are defined as relations over the state. We use binary postconditions relating pre-/post-states to make certain proofs easier.

5.2 The Linearizability Hoare Logic

LHL consists of three judgments: (1) the module judgment VE,VF,,𝒢,P,QM groups all side conditions and implies our linearizability condition (V_EM_F)V_F, (2) the program judgment ,𝒢{P}e{Q} verifies the correctness of the method body, and (3) the commit judgment 𝒢α{P}m{Q} handles individual underlay events and applies possibility updates.

5.3 Module Judgment

To establish module correctness, the prover needs to find pre-/post-conditions for each fF and rely/guarantee relations, and show the following:

where Inv():=fFInv(f), Ret():=fFRet(f). The first few conditions are standard rely-guarantee conditions, respectively: that [α] is reflexive and self-stable, that threads respect each other’s relies, and that method pre-conditions are stable. Other than the standard rely-guarantee side conditions, we require for all methods that (1) the initial proof state satisfies its pre-condition, (2) the required Hoare triples can be established for it, and (3) its post-condition implies the pre-condition of any method, so the system can still run safely afterwards.

Example 7 (Verifying the Exchanger Object).

For the exchanger, we only need to find one pre-/post-condition for the exch method, which we made into an invariant I that holds at any step in all threads. It consists of three disjuncts that hold at different stages of the exchanger: when a thread makes an exchange offer, when the other thread accepts the offer, and when the offering thread clears the offer. They mainly relate the concrete state to the linearized state. For simplicity, we use a singleton possibility, as no future-dependent linearization is required here. For example, the accepted predicate requires the prover to linearize the response ρR(α) to the exch method made by the offering thread α when the CAS cell stores the Acc state.

I(s,ρ):=α,β,v,w.offered(α,v)(s,ρ)accepted(α,β,v,w)(s,ρ)cleared(s,ρ)
offered(α,v)(s,ρ):=sCAS=Off(α,v)ρS=({},{})ρC(α)=exch(v)ρR(α)=
accepted(α,β,v,w)(s,ρ):=sCAS=Acc(α,β,v,w)ρS=({},{})
ρC(α)=exch(v)ρR(α)=exch(v).w
cleared(s,ρ):=sCAS=ρS=({},{})

The invariant obviously satisfies all conditions except the rely-guarantee-related ones and the Hoare triple, which we supply and prove later in this section.

5.4 Program Judgment

The program judgment consists of the following Hoare-style rules:

These core rules are applicable to all programs. In practice, though, the user will define control structures along with their corresponding derived rules, and the core rules will not be used directly. For instance, bind and call may be defined, and their derived rules are as follows.

5.5 Commit Judgment

The final component of the proof is the commit judgment Gi{P}m{Q} generalizing linearization points into possibility updates.

The commit judgment asks the following: for any state (s,ρ¯) satisfying the pre-condition P and any t reachable from s through the operational semantics, the user needs to produce new linearizations σ¯ reachable after finitely many possibility updates (ρ¯σ¯, defined in § 4.4) and ensure the postcondition and guarantee relation are satisfied. In LHL, these updates are restricted to CCommit or RCommit; the necessary Invoke and Return updates are applied automatically by the logic. The central proof obligation of LHL is to manage the set of possibilities ρ¯ and maintain a sufficient set of correct linearizations. The user uses the commit judgment to update the linearization as they verify an operation, until they reach the postcondition Qf. After this, the user will have to show that the series of commits they performed resulted in a linearization with the same return value as that produced by the operational semantics.

Example 8 (Verifying the Exchanger Object Cont.).

We take line 5 in the exchanger code in Fig. 1 as an example:

Here, we only consider the success branch of the CAS operation, and for simplicity, we only use a singleton possibility. Per the commit judgment, we need to prove:

We observe that the transition from s to t makes the accepted case of the precondition impossible, so we are left with offered(α,v) as the precondition. We choose to use ({},{}) as σ (the same as ρ) and perform the possibility update (ρσ) by making two transitions with event labels 𝜶:exch(v) and 𝜶:exch(v).. The return value of this linearization is , and the actual return value of this branch is also , so this linearization is valid. This possibility update is also safe because no concurrent exchange operation will successfully exchange its value for v and the new proof state satisfies the cleared disjunct of the post-condition.

Formally, the following rely-guarantee definition justifies this update, which matches the 𝒢fail relation.

Fig. 6 shows how these relations move the proof state among the three disjuncts of the invariant.

Figure 6: Logical transitions used in the proof: each node represents the assertion satisfied by the proof state, and each edge is labelled with the subscript of the guarantee condition.

After the current thread makes the offer, other threads can only perform the accept transition 𝒢acc, so the precondition in the Hoare triple is stable. The post-condition is also stable because no guarantee condition can step outside the invariant.

Other commands are verified similarly, and together, they establish the entire Hoare triple for the exch method:

[α],𝒢[α]{I;Inv[α]()}M[α]exch{I}

which discharges the remaining obligation in the module judgment of the exchanger. We refer readers to the extended version and the Rocq mechanization for more details.

5.6 Further Examples

Other than the elimination-backoff stack, we also verified (1) a family of lock-protected objects, proving correct the pattern of protecting operations of any racy but atomic objects with locks, and (2) the one-shot write-snapshot object [5], which is an interval linearizable object that no existing system can verify.

5.6.1 Lock-Protected Objects

One of the most common approaches to lift a sequential implementation of an object to an atomic linearizable concurrent object is a lock-protected critical region. Sequential implementations are prone to races when running unprotected in a concurrent setting. We prove that for any sequential but racy object E, if we enclose each of its operations with a pair of lock operations acq and rel, the overlay object built above the lock and E is atomic linearizable.

For example, Fig. 9 shows the structure of an atomic counter built above the lock and a racy counter. The racy counter specification (Fig. 9) adds an undefined behavior state (UB) to the atomic version, which would be entered whenever a concurrent access takes place. While in the UB state, any transition back into UB is allowed (as long as it does not break local sequentiality). The main proof obligation in verifying the atomic counter, i.e., ((V_LockV_RCounter)M_Counter)V_Counter, is to ensure that the racy counter would never enter the UB state. Thanks to observational refinement, the lock specification (Fig. 9) ensures the atomic counter would never observe concurrent acqs – we gain mutual exclusion of acqrel pairs “for free”. As racy counter accesses only occur inside the critical region, concurrent counter accesses are never observed, and thus we show the overlay counter is safe and atomic.

Figure 7: Module structure of the atomic counter. RCounter and Counter denote the racy counter and the atomic counter, respectively.
Figure 8: Lock specification.
Figure 9: Racy counter specification. Atomic counter specification can be obtained by removing the UB state.

Instead of directly performing the above proof for the counter object, we first prove Theorem 9 holds for racy objects in general and then instantiate it with the racy counter.

Theorem 9.

If V_E is an atomic LTS (invocations are immediately followed by responses of the same thread), Racy(V_E) is its racy version (where concurrent invocations lead to the UB state), and M_E lifts operations in Racy(V_E) with acqrel pairs, then ((V_LockRacy(V_E))M_E)V_E.

We also verified a ticket lock implementation satisfying the lock specification in Fig. 9, which can then be connected with any racy object to provide a correct implementation of a system such as the one in Fig. 9.

5.6.2 Write-Snapshot

Figure 10: Interval-sequential spec. of the write-snapshot.

Snapshot objects are common in distributed systems – for instance, in implementing a consensus protocol. The particular write-snapshot implementation we verify [5] is our example of an interval linearizable object, which no existing system can verify. In addition, we are able to later use such objects, integrating them as components into other systems using vertical and horizontal composition. The specification of the write-snapshot is presented in Fig. 10.

In our system, interval-sequential specs have no restriction on the ordering of events. For instance, the spec in Fig. 10 validates the trace:

𝜶:ws(3)𝜷:ws(5)𝜶:ws(3).{3,5}𝜸:ws(2)𝜷:ws(5).{3,5,2}

5.7 Soundness and Completeness

We prove our program logic to be both sound (Theorem 10) and complete (Theorem 11) with regard to compositional linearizability. We outline the proofs here.

Theorem 10 (Soundness).

If V_E,V_F,,𝒢,P,QM, then VEMVF.

Proof.

Recall that V_EM is a transition system which runs the code of M on top of V_E, i.e., there are four kinds of steps that can be made – an overlay call, an overlay return, an underlay call, or an underlay return. To show V_EMVF, we can show that for every transition in V_EM, there are zero or more corresponding transitions in V_F such that the two executions have the same overlay events. The module judgment and commit judgments directly provide the mappings from each individual step of V_EM to V_F, so for the soundness proof the task is to assemble those individual steps into a full execution.

We maintain that at all steps, for each thread α, the following invariants hold for the proof state (s,ρ¯):

  1. 1.

    If the thread state of α is Idle, then for all fF, P[α]f(s,ρ¯) holds.

  2. 2.

    If the thread state of α is Contfp for some fF, then there exists some I such that the following holds.

    [α],𝒢[α]{P[α]f(s,ρ¯);Inv(f);I}p{Q[α]f}(P[α]f(s,ρ¯);Inv(f);I)(s,ρ¯)Stable([α]f,I)
  3. 3.

    If the thread state of α is UCallfgk for some fF and gE, then there should exist some I and S such that the following holds.

    [α],𝒢[α]{𝒢[α]fα{P[α]f(s,ρ¯);Inv(f);I}g{S}P[α]f(s,ρ¯);Inv(f);I;S}p{Q[α]f}Stable([α]f,I)Stable([α]f,S)(P[α]f(s,ρ¯);Inv(f);I)(s,ρ¯)

The proof then steps through the execution of V_EM, cycling through the invariants in order for each thread.

For the overlay steps, we appeal to the precondition and postcondition of the program judgment. The module judgment requires the user to verify the operations with precondition P[α]f;Inv(f) and postcondition Q[α]f. Inv(f) is defined to make a simultaneous step in the operational semantics and the linearization, which takes care of that proof obligation. Finally, the module judgment has a side condition which asks the user to prove that a simultaneous overlay return and linearization return may be made if Q[α]f holds of the state. This side-condition is present in the mechanization but omitted here, as it is almost always trivial but complicates the presentation.

For the underlay steps, recall that the commit judgment requires the user to directly provide linearization steps for the underlay event. Thus for this case we update I or supply S, and then simply add the provided steps to the linearization.

Once we assemble linearizations for the execution of each operation, we can chain those linearizations together to form a full linearization for the operational semantics of the entire concurrent object, proving VEMVF.

Theorem 11 (Completeness).

If VEMVF, then

𝒢PQ.V_E,V_F,,𝒢,P,QM
Proof.

For completeness, we note that the possibility update rules in Fig. 4 may be extended to be labelled by the same events as in Fig. 3. This extension is completely determined by the particular instantiation of the update rules. For example, if in Invoke, we have that ρC goes to ρC[αm], then the corresponding event is the overlay invocation 𝜶:m. As a result, given a sequence of possibility updates q, we may take its projection q,F to the overlay events implied by the rule applications (Invoke and Return rules), or its projection qE, to only underlay events (CCommit or RCommit).

Completeness requires constructing , 𝒢, P, and Q such that the program logic judgment holds. To do this, we define an invariant I(s,ρ¯), which maintains that there exists a trace p (of the operational semantics in Fig. 3) such that the following three propositions hold.

  1. 1.

    The current state s is reachable by transitioning from the initial state to s along p following the operational semantics.

  2. 2.

    For every possibility ρ in ρ¯, ρ is reachable through possibility updates q with the same overlay events as p (i.e., q,F=p,F).

  3. 3.

    For every possibility σ reachable by transitioning along the linearized specification on a trace q with the same overlay events as p (i.e., q,F=p,F), there is a prefix l of q such that a possibility ρ has transitioned along l, and there is a trace r such that ρ has transitioned along r into σ.

Informally, I maintains that the set of possibilities at any point contains exactly all possibilities that are still valid w.r.t. the current concrete trace, and that are reachable in the linearized specification. The rely-guarantee conditions are then defined using I as follows:

From here we must prove the actual program logic judgments. The side-conditions on I, such as stability, are straightforward due to how I is defined. The key obligation of the program judgment is then to show that steps maintain the invariant. To ensure this, upon each commit, we advance the possibility set as far as possible, populating it with every possible linearization of the current execution that satisfies the invariant. Once we reach the end of the function, we perform the return step by again advancing the possibilities in the set as far as possible, and then pruning all the possibilities which are not ready to make the overlay return. The assumption that the program is already linearizable is key here, as it allows us to prove that after the pruning is complete, the possibility set will still be non-empty, allowing us to complete the overlay return step.

6 Related Work

6.1 Elimination-Backoff Stack and Write-Snapshot

Ours is the first paper with the technology to mechanize the proof of linearizability of the one-shot write-snapshot [5] object against its interval-sequential specification, and we believe we are also the first to give a truly modular proof of the EBStack. We compare other works that have verified the EBStack at the end of this section.

6.2 Possibility-Based Reasoning

Our approach is mainly inspired by [16], [21], and [27]. [16] not only proposed the notion of linearizability but also introduced possibility reasoning as an axiomatic method for proving atomic linearizability, and showed it is complete and sound for proofs of linearizability for individual traces. Their original technique has been recently mechanized by [17].Their technique focuses only on atomicity, however, and only on verifying a single component, and does not provide the ability to verify large object systems either modularly or compositionally. Our approach is strictly more general in that it handles non-atomic objects and is compatible with theirs, so we could have verified any example they can verify.

[21] packaged possibility reasoning in a program logic for the first time, and used interval partial orders to represent a set of possibilities of an execution. [26, 27] show that [21]’s program logic is not complete for atomically linearizable objects, and extend it further to support compositional linearizability. They conjectured that their program logic was complete due to its relationship with possibility reasoning. Neither work has been mechanized. Our program logic mechanization builds upon their theory, and extends the possibilities from [27] with state, making possibility reasoning simpler and more usable than using either partial orders or traces, and embeds this method into a mechanized program logic. We provide a further extensive comparison with the closely related work of [27] in the extended version of the paper [14].

6.3 Other Works

It is worth mentioning that other than Herlihy-Wing possibilities, [28] have provided a technique based on forward and backward simulation that is sound and complete for Herlihy-Wing linearizability. Backward simulations are often deemed hard to reason about, however, because they go against the natural forward flow of executions, introduce backward non-determinism, require knowledge of the future, and often require introducing history variables to provide information that would be available under a forward technique. A similar approach is that of [6], who prove a direct simulation between the EB stack and its abstract model. However, this simulation is based on the execution of the entire thread pool. When showing the simulation, the prover needs to discuss the execution of all threads. In contrast, our approach and other program logics focus on enabling thread-local reasoning, which is more accessible to programmers and makes proofs more intuitive.

Regarding the mechanization, the closest work to ours is [22]. It focuses on using linearizability to verify serializability. Our encoding of modules as interaction trees is the same as theirs, and some aspects of their treatment of atomic linearizability are compatible with ours. Despite that, their framework is restricted to atomicity. This means that their LTSs pair call and return events and cannot handle non-atomic objects. As a result, they can’t compute overlay objects (VM), as these can be non-atomic even if V is atomic, so they define an object as a pair of an underlay (atomic) LTS and a module instead. Meanwhile, an object for us is just an LTS. While they prove several compositional properties of the modules, which boil down to properties of interaction trees, we prove several more properties related to computing overlay objects, monotonicity w.r.t. trace refinement, etc. These enable the compositional linearizability framework and make up a significant portion of our mechanization. Due to the compatibility of their approach with ours, we believe that their results around encoding serializability as atomic linearizability could be carried over.

Although previous linearizability-based works cannot support non-atomic objects like the exchanger, it is still possible to use Hoare triples to give them a tight specification. [29] specifies the functional correctness of the exchange operation through Hoare triples and develops a Hoare logic verifying the exchanger in Rocq. However, due to the limitation of functional correctness, their Hoare triple specification can only be used when the execution of all interleaving threads is known, i.e., composed through the parallel composition rule. In contrast, our work focuses on object-level verification, where the client would access the object in an arbitrary concurrent environment. Moreover, [29] only verified a simple example as the client of the exchanger, instead of a complex object such as the elimination-backoff stack. It is unclear whether their logic can appropriately handle complex examples such as the helping mechanism in the elimination-backoff stack or future-dependent linearizations.

6.4 Logical Atomicity

Logical atomicity [7, 19] uses logically atomic Hoare triples Pop()Q to specify the ability of the given operation to perform the update abstracted as a transition from assertion P to assertion Q at a single physically atomic statement. Since its focus is on atomicity, it cannot generally specify set and interval linearizable objects satisfactorily. It is compositional as one may directly use the logically atomic Hoare triple in proofs of other functions that use this operation. It is known that a logically atomic triple implies Herlihy-Wing linearizability [2], but there is no evidence that logical atomicity can be used to specify all Herlihy-Wing linearizable objects, so its completeness is unknown. Moreover, logically atomic triples rely on a concrete assertion language, a concrete programming language, and their underlying semantic models. This limits the compositionality to within its own program logic framework, while our approach can specify any object using an abstract LTS, without assuming the actual semantic model or the program logic verifying the object against the specification.

6.5 Contextual Refinement

Contextual refinement provides an alternative to linearizability. It uses an abstract piece of code A_E to specify the concrete implementation M_E by requiring that the behavior of M_E running in any context C is bounded by the behavior of A_E running in the same context, usually denoted as M_EA_E. Contextual refinement is equivalent [9] to Herlihy-Wing linearizability and supports vertical composition in a sequential setting [30]. However, contextual refinement relies on a concrete programming language for the abstract code, and an embedding of this language into the original language for the concrete code is necessary for composing a concrete context with an “abstract” specification. In contrast, our framework only uses the signature of an object and applies to any programming language whose behaviors are characterized by call and return events, without any modification to the language. This is a major benefit of verifying objects directly against linearizability specifications. Moreover, there has been no work to support modular verification of non-atomic objects like the exchanger and the interval-linearizable write-snapshot within the contextual refinement framework.

6.6 Relational Hoare logic

Relational Hoare logic [1] is widely used to show contextual refinement. [23] used local rely-guarantee reasoning to implement a relational logic that verifies the contextual refinement of concurrent objects w.r.t. their atomic abstract code. [35] later applied this technique to verify an operating system using their relational program logic. They achieved this using logically atomic triples [7] to establish a contextual refinement between the two. Their work allows modular verification, but with limited encapsulation: an object’s abstract code A_F must also include all its sub-components’ abstract code A_E. They can verify sub-components against corresponding abstract code A_E and use their Hoare triples to execute A_E embedded in A_F. Consequently, the top-level specification consists of very complicated abstract code that does not intuitively specify its behaviors. In contrast, our LTS specification encapsulates all details of the underlay object and provides an implementation-agnostic interface for users to verify overlay objects.

[31] encodes the contextual refinement obligation into a ghost state to verify it with CSL. Iris [19] is a higher-order logic for verifying fine-grained concurrent programs and also features logical atomicity. It leads to the ReLoC framework [10, 11, 33], which shows contextual refinement of concurrent programs using a program logic, with prophecy variables [20] enabling future-dependent linearization. ReLoC achieves compositionality by first encapsulating reusable code into smaller functions, then verifying these functions against logically atomic Hoare triples encoded as a logical relation, and lastly verifying the refinement of the top-level object using these intermediate proofs. However, it is unknown how to reuse the top-level refinement specification when verifying objects that use this top-level object. As a result, every time they build new objects using the one verified against the refinement specification, they need to prove a different functional correctness specification before they can reuse its proof. In contrast, our framework imposes LTS specifications uniformly at any level of abstraction.

As shown in Table 1, both lines of work have verified the elimination-backoff stack but failed to achieve the same level of compositionality as we do. [23] directly inlined all underlay code in the EBStack code. ReLoC [10] broke down the exchanger into three atomic operations: make_offer, accept_offer, and revoke_offer, exposing implementation details, and implemented the EBStack using these operations. They built and verified the EBStack using these three methods and their specifications. This effectively inlines a specific protocol for implementing the exchanger object within the stack implementation. As a result, to verify other objects using the exchanger, nothing except the specifications for these low-level atomic operations can be salvaged and reused from their proof. They still need to inline all the implementation code of the exchanger in the overlay object. In contrast, our approach allows direct composition above the ready-to-use exchanger implementation and specification.

7 Evaluation and Conclusion

Table 1 shows the evaluation of our program logic. There are mainly two other lines of work that mechanized the verification of the elimination-backoff stack. Neither can handle non-atomic objects, such as the exchanger and the elimination array, compositionally, and must embed them in their stack code, while LHL allows compositional verification of all three objects.

Table 1: Evaluation of the elimination-backoff stack proof in LHL and other frameworks and lines of Rocq code of LHL. We use for not verified, for verified but with sub-modules inlined, and for compositionally verified. The Elimination Array requires no lines of Spec because the Spec is the same as for the Exchanger. () Both R-LRG and ReLoC can verify the try stack as a standalone object but they chose not to in their formalization. () The elimination array uses the same specification as the exchanger.
EBStack Exchanger
Elimination
Array
TryStack
R-LRG[23]
ReLoC[11]
LHL
LOC
of LHL
in Rocq
Spec 50 38 0 38
Code 26 28 4 21
Proof 4056 4244 6230 3046
Sound Complete Definitions & Lemmas
1077 412 812

Although we have simplified possibility reasoning using LTS instead of partial orders and traces, the Rocq proof burden still turns out to be significant. As Table 1 indicates, the proofs are admittedly lengthy, reflecting the inherent complexity of reasoning about concurrent behavior. This is a consequence of a few factors. First, we did not design the program logic for ease of use, instead attempting to ensure that the proofs of soundness and completeness are easier to write. Nonetheless, a more usable program logic could be implemented and validated by translating its proofs into proofs in our more fine-grained logic. A second issue is that we have not implemented many tactics and auxiliary automation. We view proof automation as orthogonal to our contribution; our logic provides the semantic foundations upon which future automation can be built. The major difficulty we encounter in the soundness and completeness proofs is reasoning about execution traces. Even though we can hide traces from logic users using LTS, the internal proof still requires direct management of traces. We believe we can solve these issues with more automation, building on top of the foundation of LHL laid by this paper.

References

  • [1] Nick Benton. Simple relational correctness proofs for static analyses and program transformations. ACM SIGPLAN Notices, 39(1):14–25, 2004. doi:10.1145/964001.964003.
  • [2] Lars Birkedal, Thomas Dinsdale-Young, Armaël Guéneau, Guilhem Jaber, Kasper Svendsen, and Nikos Tzevelekos. Theorems for free from separation logic specifications. Proceedings of the ACM on Programming Languages, 5(ICFP):1–29, 2021. doi:10.1145/3473586.
  • [3] Elizabeth Borowsky and Eli Gafni. Immediate atomic snapshots and fast renaming (extended abstract). In Jim Anderson and Sam Toueg, editors, Proceedings of the Twelth Annual ACM Symposium on Principles of Distributed Computing, Ithaca, New York, USA, August 15-18, 1993, pages 41–51. ACM, 1993. doi:10.1145/164051.164056.
  • [4] Jeremy S Bradbury, James R Cordy, and Juergen Dingel. Mutation operators for concurrent java (j2se 5.0). In Second Workshop on Mutation Analysis (Mutation 2006-ISSRE Workshops 2006), pages 11–11. IEEE, 2006.
  • [5] Armando Castañeda, Sergio Rajsbaum, and Michel Raynal. Specifying concurrent problems: Beyond linearizability and up to tasks. In Proceedings of the 29th International Symposium on Distributed Computing - Volume 9363, DISC 2015, pages 420–435, Berlin, Heidelberg, 2015. Springer-Verlag. doi:10.1007/978-3-662-48653-5_28.
  • [6] Robert Colvin and Lindsay Groves. A scalable lock-free stack algorithm and its verification. In Fifth IEEE International Conference on Software Engineering and Formal Methods (SEFM 2007), pages 339–348. IEEE, 2007. doi:10.1109/SEFM.2007.2.
  • [7] Pedro da Rocha Pinto, Thomas Dinsdale-Young, and Philippa Gardner. Tada: A logic for time and data abstraction. In Richard E. Jones, editor, ECOOP 2014 - Object-Oriented Programming - 28th European Conference, Uppsala, Sweden, July 28 - August 1, 2014. Proceedings, volume 8586 of Lecture Notes in Computer Science, pages 207–231. Springer, Springer, 2014. doi:10.1007/978-3-662-44202-9_9.
  • [8] Mike Dodds, Andreas Haas, and Christoph M. Kirsch. A scalable, correct time-stamped stack. In Proceedings of the 42nd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL ’15, pages 233–246, New York, NY, USA, 2015. Association for Computing Machinery. doi:10.1145/2676726.2676963.
  • [9] Ivana Filipovic, Peter W. O’Hearn, Noam Rinetzky, and Hongseok Yang. Abstraction for concurrent objects. In Giuseppe Castagna, editor, Programming Languages and Systems, 18th European Symposium on Programming, ESOP 2009, Held as Part of the Joint European Conferences on Theory and Practice of Software, ETAPS 2009, York, UK, March 22-29, 2009. Proceedings, volume 5502 of Lecture Notes in Computer Science, pages 252–266, Berlin, Heidelberg, 2009. Springer. doi:10.1007/978-3-642-00590-9_19.
  • [10] Dan Frumin, Robbert Krebbers, and Lars Birkedal. Reloc: A mechanised relational logic for fine-grained concurrency. In Anuj Dawar and Erich Grädel, editors, Proceedings of the 33rd Annual ACM/IEEE Symposium on Logic in Computer Science, LICS 2018, Oxford, UK, July 09-12, 2018, pages 442–451. ACM, 2018. doi:10.1145/3209108.3209174.
  • [11] Dan Frumin, Robbert Krebbers, and Lars Birkedal. Reloc reloaded: A mechanized relational logic for fine-grained concurrency and logical atomicity. Log. Methods Comput. Sci., 17(3), 2021. doi:10.46298/lmcs-17(3:9)2021.
  • [12] Éric Goubault, Jérémy Ledent, and Samuel Mimram. Concurrent specifications beyond linearizability. In Jiannong Cao, Faith Ellen, Luís Rodrigues, and Bernardo Ferreira, editors, 22nd International Conference on Principles of Distributed Systems, OPODIS 2018, Hong Kong, December 17-19, 2018, volume 125 of LIPIcs, pages 28:1–28:16. Schloss Dagstuhl – Leibniz-Zentrum für Informatik, 2018. doi:10.4230/LIPIcs.OPODIS.2018.28.
  • [13] Eashan Hatti, Arthur Oliveira Vale, Zhongye Wang, Yueyang Feng, and Zhong Shao. Linearizability Hoare Logic. Software, NSF 2019285, NSF 2313433, NSF 2442888, DARPA Agreement No. HR00112590130, swhId: swh:1:dir:ee421dd7fa7166ad053af08aa59a2cf82c194b33 (visited on 2026-06-10). URL: https://github.com/ehatti/LHL/tree/ecoop-camera-ready, doi:10.4230/artifacts.26388.
  • [14] Eashan Hatti, Arthur Oliveira Vale, Zhongye Wang, Yueyang Feng, and Zhong Shao. A complete program logic for compositional linearizability. Technical Report YALEU/DCS/TR-1577, Yale University, 2026. URL: https://flint.cs.yale.edu/publications/lhl.html.
  • [15] Danny Hendler, Nir Shavit, and Lena Yerushalmi. A scalable lock-free stack algorithm. In Proceedings of the sixteenth annual ACM symposium on Parallelism in algorithms and architectures, pages 206–215, 2004. doi:10.1145/1007912.1007944.
  • [16] Maurice Herlihy and Jeannette M. Wing. Linearizability: A correctness condition for concurrent objects. ACM Trans. Program. Lang. Syst., 12(3):463–492, July 1990. doi:10.1145/78969.78972.
  • [17] Prasad Jayanti, Siddhartha Jayanti, Ugur Yavuz, and Lizzie Hernandez. A universal, sound, and complete forward reasoning technique for machine-verified proofs of linearizability. Proc. ACM Program. Lang., 8(POPL), January 2024. doi:10.1145/3632924.
  • [18] Cliff B. Jones. Tentative steps toward a development method for interfering programs. ACM Trans. Program. Lang. Syst., 5(4):596–619, October 1983. doi:10.1145/69575.69577.
  • [19] Ralf Jung, Robbert Krebbers, Jacques-Henri Jourdan, Aleš Bizjak, Lars Birkedal, and Derek Dreyer. Iris from the ground up: A modular foundation for higher-order concurrent separation logic. Journal of Functional Programming, 28:e20, 2018. doi:10.1017/S0956796818000151.
  • [20] Ralf Jung, Rodolphe Lepigre, Gaurav Parthasarathy, Marianna Rapoport, Amin Timany, Derek Dreyer, and Bart Jacobs. The future is ours: prophecy variables in separation logic. Proceedings of the ACM on Programming Languages, 4(POPL):1–32, 2019. doi:10.1145/3371113.
  • [21] Artem Khyzha, Mike Dodds, Alexey Gotsman, and Matthew Parkinson. Proving linearizability using partial orders. In Programming Languages and Systems: 26th European Symposium on Programming, ESOP 2017, Held as Part of the European Joint Conferences on Theory and Practice of Software, ETAPS 2017, Uppsala, Sweden, April 22–29, 2017, Proceedings 26, pages 639–667. Springer, 2017. doi:10.1007/978-3-662-54434-1_24.
  • [22] Mohsen Lesani, Li-yao Xia, Anders Kaseorg, Christian J. Bell, Adam Chlipala, Benjamin C. Pierce, and Steve Zdancewic. C4: verified transactional objects. Proc. ACM Program. Lang., 6(OOPSLA1), April 2022. doi:10.1145/3527324.
  • [23] Hongjin Liang and Xinyu Feng. Modular verification of linearizability with non-fixed linearization points. In Proceedings of the 34th ACM SIGPLAN Conference on Programming Language Design and Implementation, pages 459–470, 2013. doi:10.1145/2491956.2462189.
  • [24] Nancy A. Lynch and Frits W. Vaandrager. Forward and backward simulations, II: timing-based systems. Inf. Comput., 128(1):1–25, July 1996. doi:10.1006/inco.1996.0060.
  • [25] Gil Neiger. Set-linearizability. In Proceedings of the Thirteenth Annual ACM Symposium on Principles of Distributed Computing, PODC ’94, page 396, New York, NY, USA, 1994. Association for Computing Machinery. doi:10.1145/197917.198176.
  • [26] Arthur Oliveira Vale, Zhong Shao, and Yixuan Chen. A compositional theory of linearizability. Proc. ACM Program. Lang., 7(POPL), January 2023. doi:10.1145/3571231.
  • [27] Arthur Oliveira Vale, Zhong Shao, and Yixuan Chen. A compositional theory of linearizability. J. ACM, 71(2), April 2024. doi:10.1145/3643668.
  • [28] Gerhard Schellhorn, John Derrick, and Heike Wehrheim. A sound and complete proof technique for linearizability of concurrent data structures. ACM Trans. Comput. Logic, 15(4), September 2014. doi:10.1145/2629496.
  • [29] Ilya Sergey, Aleksandar Nanevski, Anindya Banerjee, and Germán Andrés Delbianco. Hoare-style specifications as correctness conditions for non-linearizable concurrent objects. ACM SIGPLAN Notices, 51(10):92–110, 2016. doi:10.1145/2983990.2983999.
  • [30] Youngju Song, Minki Cho, Dongjae Lee, Chung-Kil Hur, Michael Sammler, and Derek Dreyer. Conditional contextual refinement. Proceedings of the ACM on Programming Languages, 7(POPL):1121–1151, 2023. doi:10.1145/3571232.
  • [31] Aaron Turon, Derek Dreyer, and Lars Birkedal. Unifying refinement and hoare-style reasoning in a logic for higher-order concurrency. In Proceedings of the 18th ACM SIGPLAN international conference on Functional programming, pages 377–390, 2013. doi:10.1145/2500365.2500600.
  • [32] Viktor Vafeiadis. Modular fine-grained concurrency verification. PhD thesis, University of Cambridge, UK, July 2008. doi:10.48456/tr-726.
  • [33] Simon Friis Vindum, Dan Frumin, and Lars Birkedal. Mechanized verification of a fine-grained concurrent queue from meta’s folly library. In Proceedings of the 11th ACM SIGPLAN International Conference on Certified Programs and Proofs, pages 100–115, 2022. doi:10.1145/3497775.3503689.
  • [34] Li-yao Xia, Yannick Zakowski, Paul He, Chung-Kil Hur, Gregory Malecha, Benjamin C. Pierce, and Steve Zdancewic. Interaction trees: representing recursive and impure programs in Coq. Proc. ACM Program. Lang., 4(POPL), December 2019. doi:10.1145/3371119.
  • [35] Fengwei Xu, Ming Fu, Xinyu Feng, Xiaoran Zhang, Hui Zhang, and Zhaohui Li. A practical verification framework for preemptive os kernels. In International Conference on Computer Aided Verification, pages 59–79. Springer, 2016. doi:10.1007/978-3-319-41540-6_4.