An Abstraction Technique for Verifying Shared-Memory Concurrency

: Modern concurrent and distributed software is highly complex. Techniques to reason about the correct behaviour of such software are essential to ensure its reliability. To be able to reason about realistic programs, these techniques must be modular and compositional as well as practical by being supported by automated tools. However, many existing approaches for concurrency veriﬁcation are theoretical and focus primarily on expressivity and generality. This paper contributes a technique for verifying behavioural properties of concurrent and distributed programs that balances expressivity and usability. The key idea of the approach is that program behaviour is abstractly modelled using process algebra, and analysed separately. The main difﬁculty is presented by the typical abstraction gap between program implementations and their models. Our approach bridges this gap by providing a deductive technique for formally linking programs with their process-algebraic models. Our veriﬁcation technique is modular and compositional, is proven sound with Coq, and has been implemented in the automated concurrency veriﬁer VerCors. Moreover, our technique is demonstrated on multiple case studies, including the veriﬁcation of a leader election protocol.


Introduction
Modern software is typically composed of multiple concurrent components that communicate via shared or distributed interfaces, for example via shared-memory or via message passing. The concurrent nature of the interactions between (sub)components makes such software highly complex as well as notoriously difficult to develop correctly. To ensure the reliability of modern software, verification techniques are much-needed to aid software developers to comprehend all possible concurrent system behaviours. To be able to reason about realistic programs these techniques must be modular and compositional, as well as be supported by automated verification tools.
Even though verification of concurrent and distributed software is a very active research field [1-6], most work in this line of research is essentially theoretical, and tends to focus primarily on contributing expressive program logics specialised in reasoning about advanced concurrency features like relaxed or weak memory, fine-grained concurrency, message passing interaction, etc. Even though expressive, it is very challenging for these logics to be integrated into SMT-based automated verifiers like for example VeriFast [7], VerCors [8] and Viper [9,10]. Instead, most of these works have to be applied in pen-and-paper style, or at best semi-automatically in the context of an interactive theorem prover like Coq [11,12] or Isabelle/HOL [13].
This article contributes a concurrency verification technique that applies directly on the level of program code and is supported by automated verifiers. However, rather than doing the verification fully on the level of program code, our approach allows soundly abstracting program behaviour into abstract models which can be reasoned about externally, on a higher level in which irrelevant implementation details are hidden, to (indirectly) prove properties about the program behaviour. The presented verification technique (1) has been implemented in VerCors-an automated SMT-based concurrency verifier; (2) is demonstrated on various (real-world) examples, including a leader election protocol (presented in Section 5); and (3) the metatheory of the technique has been fully formalised and proven sound with the Coq proof assistant. With respect to (2); apart from the examples given in this article, more examples of our approach are given in [14], including the verification of a (reentrant) lock as well as a concurrent parallel GCD algorithm. Our technique has also been used in a real-world industrial case study [15]-on the formal verification of a safety-critical traffic tunnel control system. This article extends our earlier VMCAI'20 article [16]. Elaborating on the contributions with respect to this earlier article; we contribute a generalisation of the theory in [16] by combining it with the techniques proposed in [17] and [18] into a single logical framework that is more general than the original. This combined unified framework is proven sound with Coq and is available online at [19].

Motivation
Reasoning about complex concurrent program behaviours is only practical if conducted at a suitable level of abstraction that hides implementation details that are irrelevant for the properties to prove. Furthermore, any real concurrent programming language with shared memory, threads and locks, has only very little algebraic behaviour. In contrast, process algebras offer an abstract, mathematically elegant way of expressing program behaviour. Process algebras have been used widely in the past for modelling and analysing the behaviour of concurrent programs at an adequate level of abstraction [20,21]. Our approach therefore uses a process algebra as a language for specifying program behaviour. Such a specification can be seen as a model, the properties of which can additionally be checked (say by interactive theorem proving, or by model checking against temporal logic formulas, which can be seen as even more abstract behavioural specifications). The main difficulty of this approach is dealing with the typical abstraction gap between program implementations and their abstract models. The unique contribution of our approach is that it bridges this gap by providing a deductive technique for formally linking programs with their process-algebraic models. These formal links preserve safety properties [22]; we leave the preservation of liveness properties for future work.
The key idea of the approach rests in the use of concurrent separation logic (CSL) to reason not only about data races and memory safety, which is standard [23,24], but also about process-algebraic models (that is, specified program behaviours), viewing the latter as resources that can be split and consumed. This results in a modular and compositional approach to establish that a program behaves as specified by its abstract model. Our approach is formally justified by (mechanically proven) correctness results stating that any verified program is a refinement of its abstract, process-algebraic model.
Process-algebraic models are composed out of individual actions that abstract atomic behaviours of program components. Our approach allows specifying program components to follow a particular sequence/pattern of actions-a protocol. One can then reason about the interaction behaviour of different program components by reasoning about the composition of their models, for example by using a model checker for process algebra, like mCRL2 [25]. This approach of specifying the interactions of program components is different from classical Hoare logic, which is purely transformational in the sense that it considers verified (terminating) program components essentially as transformers from states satisfying the specified precondition to states satisfying the specified postcondition.
A benefit of our combined approach compared to model checking is that it allows reasoning soundly about both data and control-oriented properties in a single framework. Model checkers typically specialise in reasoning about temporal, control-oriented specifications (e.g., send actions must always be matched by a recv), and generally have limited support for handling data due to the risk of state-space explosions. Hoare logic based techniques, on the other hand, tend to specialise in reasoning about data specifications (e.g., a sorting function should yield a sorted permutation of its input), and are typically limited in their capabilities to reason about control-flow properties. Since realistic concurrent systems often deal with both data and control-flow, it is beneficial to be able to reason about both in a single framework. Additionally, our technique addresses the typical "abstraction gap" problem of model checking: is the model actually a faithful abstraction of the modelled system? We propose techniques to formally link programs to their abstract models, allowing one to prove that all program behaviours that should be captured by the abstract model are indeed soundly abstracted.

Contributions
The main contributions of this extended article are: • A verification technique to reason about the behaviour of shared-memory concurrent programs that is modular, compositional, and proven sound. This article extends [16] by generalising its verification technique and combining it with the core ideas of [17,18]. In particular, it extends the process algebra specification language with summations, support for input parameters, and the assertional processes of [17], which shall all be introduced later, in Section 3.
• A full Coq development of the formalisation as presented in Section 3, together with a soundness proof of the approach. The Coq sources and their documentation are available at [19].
• Several examples that demonstrate this new (unified) verification approach, including a leader election protocol case study discussed in Section 5.

Outline
The remainder of this article is organised as follows. First Section 2 illustrates our technique on a small Owicki-Gries example program, before Section 3 gives theoretical justification of the verification technique. In particular, Section 3.1 introduces the process algebra specification language, after which Section 3.2 introduces the programming language on which the approach is formalised on. Section 3.3 defines and discusses the syntax and semantics of the assertion language, which is a concurrent separation logic with special constructs to to handle process-algebraic models. Section 3.4 discusses the proof system and Section 3.4 its soundness. Section 4 gives details on how the verification technique is implemented in the concurrency verified VerCors, and briefly elaborates on the Coq development. Section 5 demonstrates the approach on a larger case study: the verification of a classical leader election protocol. Finally, Section 6 discusses related work and Section 7 concludes.

Approach
Before going into the formal details of the approach, let us first illustrate it on a simple example. Our approach allows abstractly specifying concurrent program behaviour as process-algebraic models. Processes are composed of atomic, indivisible actions. In our approach the actions are logical descriptions of shared-memory modifications: they describe what changes the program is allowed to make to a specified region of shared memory-the program heap. These actions are then linked to the concrete instructions in the program code that perform the memory updates. These links between program components and their abstract models are established deductively, using a concurrent separation logic that is presented later. Well-known techniques for process-algebraic reasoning can then be applied to guarantee safety properties over all possible state changes, as described by their compositions of actions. The novelty of the approach is that these safety properties can then be relied upon in the program logic due to the established formal connection between program components and their process-algebraic models.
Define a process-algebraic model OG = (incr(4) mult(4))·?(b post ) that is composed out of two actions, incr and mult, that abstract the two atomic sub-programs; Step 2. Verify that the OG process indeed satisfies the Owicki-Gries postcondition, b post ; and Step 3.
Deductively verify that OG is a correct behavioural specification of the program's execution flow. That is, verify that every atomic state change that is executed by a run of the program has a corresponding action in OG.
The following paragraphs give more detail on these three steps. 2.1.1.
Step 1: Specifying Program Behaviour The first step is to construct a behavioural specification OG of the example program. The OG process is defined to be the parallel composition of the actions incr(4) and mult (4), which specify the behaviour of the atomic increment and multiplication in the program, respectively. In our approach, program behaviour is specified logically, by associating a contract to every action. For the example program, incr and mult would have the following contract: guard true; effect x = \old(x) + n; action incr(int n); guard true; effect x = \old(x) * n; action mult(int n); Any action contract consists of a guard and an effect. The guard of any action specifies the condition under which the action is allowed to be executed. In the above example, the guard of both incr and mult is specified to be true, meaning that both these actions may unconditionally be performed. The effect clause of any action specifies the way the action is allowed to change the (program) state. Observe that incr and mult are indeed abstractions of the two atomic sub-programs, and that the effect clauses of these actions are abstract specifications of how the program updates the heap. (Note that one could think of guards and effect of actions as pre-and postconditions, respectively. However, they are not strictly the same (hence the slightly different terminology). For the sake of process-algebraic analysis all action contracts can be assumed to hold, while on the program level one has to prove that sets of instructions that correspond to the action satisfy the action contract, as will be explained in a moment.) Note that both these abstract specifications contain a free variable x, which is a process-algebraic variable that is later linked to a concrete heap location in the program (this will be [E]). Moreover, the increment and multiplication of 4 has now been generalised to an arbitrary integer n.
These two actions may be composed into a full behavioural specification of the example program, by also assigning a top-level contract to OG: requires true; process OG(int n) := (incr(n) mult(n)) · ? x = (\old(x) + n) * n ∨ x = \old(x) * n + n ; Notice that the OG process has the form (incr(n) mult(n))·?(b post ) with b post the Owicki-Gries postcondition. Here · denotes sequential composition, and ?(b post ) is an assertion process. These assertions are the main subject of process-algebraic reasoning: we verify that all asserted properties are never violated. Here we specify that ?(b post ) holds after executing incr(n) and mult(n) in any order.
The OG process also has a precondition that could potentially impose restrictions on the values of n. But for this Owicki-Gries example we do not have any such restrictions. Note that postconditions (that is, ensures clauses) are encoded as assertional processes, like done above.

Step 2: Process-Algebraic Reasoning
The next step is to verify that OG satisfies all properties b that are encoded as assertions ?(b), which can be reduced to standard process-algebraic analysis. Intuitively we say that OG is verified if, starting from any state satisfying OG's requires clause, the process can never reach an asserted property b that does not hold. We shall later give a more formal definition of what it means for a process to be verified with respect to its precondition, in Section 3.4.2.
The standard approach to analysing OG would be to first linearise it to the bisimilar process incr(n) · mult(n) · ?(b post ) + mult(n) · incr(n) · ?(b post ), where + denotes non-deterministic choice and with b post again the Owicki-Gries postcondition, and then to reason about all branches of this linearised process. With "reasoning about all branches" we intuitively mean establishing that all assertions encountered during any execution of a process are a logical consequence of the series of effects preceding the assertion. A formal definition is provided later in Section 3.1. VerCors currently does the analysis by encoding the linearised process as input to the Viper verifier [10]. VerCors can indeed automatically prove that OG satisfies the asserted property.

Step 3: Deductively Linking Processes to Programs
The key idea of our approach is that, by analysing how contract-complying action sequences change the values of process-algebraic variables, we may indirectly reason about how the content at heap location [E] evolves over time. So the final step is to project this process-algebraic reasoning onto program behaviour, by annotating the program. Figure 1 shows the required program annotations. First, x is connected to [E] by initialising a new model M on line 2 that executes according to OG(4). The actions incr and mult are then linked to the corresponding sub-programs on lines 5-7 and 11-13 by identifying action blocks in the code, using special program annotations. We use these action annotations to verify in a thread-modular way that the left thread performs the incr(4) action (on lines 5-7) and that the right thread performs mult(4) (lines 11-13). As a result, when the program reaches the query annotation on line 15, only the ?(b post ) process is left on the process level-the incr(4) mult(4) part has already been executed alongside the program. Since the Owicki-Gries postcondition b post is already proven externally, by other means, in the previous step, the program logic may rely on its validity. But since we tracked the contents at heap location [E] on the process level as the variable x, one may indirectly conclude that the heap at location [E] has evolved as described by OG. In other words, using program annotations we prove that the program is a refinement of OG, meaning that we get the asserted property in the logic, on line 17.
Finally, the finish annotation on line 16 indicates that the model has been fully reduced at that point, and thus may be disposed of. This is for technical reasons; the program logic will do some bookkeeping while dealing with process-algebraic abstractions, and finish will cause this bookkeeping to be cleaned up. This is later discussed in greater detail, in Section 3.4.2.
Finally, the finish annotation on line 16 indicates that the model has been fully reduced at that point, and thus may be disposed of. This is for technical reasons; the program logic will do some bookkeeping while dealing with process-algebraic abstractions, and finish will cause this bookkeeping to be cleaned up. This is later discussed in greater detail, in Section 3.4.2.

Formalisation
We now give theoretical justification of the verification approach and explains the underlying logical machinery. First, Sections 3.1 and 3.2 briefly discuss the syntax and semantics of process algebraic models and programs, respectively. Then Section 3.3 presents the program logic as a concurrent separation logic with assertions that allow to specify program behaviour as a process algebraic model. Section 3.4 formally introduces and discusses the proof rules. Finally, Section 3.5 discusses soundness of the approach. All these components have been fully formalised in Coq, including the soundness proof of the logic. Section 4 elaborates on the Coq development of the meta-theory, as well as on tool support, developed for the VerCors concurrency verifier.

Definition 1 (Processes).
e ∈ ProcExpr : Clarifying the different connectives and constructs, ε is the empty process, which has no behaviour. The δ process is the deadlocked process which neither progresses nor terminates. Processes of the form a(e) are actions, which model the basic, observable (shared-memory) system behaviours. Actions are parameterised by data, in the form of expressions e. The process P · Q is the sequential composition of P and Q, whereas P + Q is their non-deterministic choice. The parallel composition of processes P and Q is written P Q. The process P Q is the left-merge of P and Q, which is similar in spirit to parallel composition, however insists that the left-most process P proceeds first. The left-merge is an auxiliary connective commonly used to axiomatise parallel composition [31], by having P Q = P Q + Q P. The process Σ x P is the infinite summation P[x/v 0 ] + P[x/v 1 ] + · · · over all values v 0 , v 1 , ... ∈ Val. Any summation Σ x P is a binder for the summation variable x. In the remainder we assume without loss of generality that all variables bound by summation are unique (since any such

Formalisation
We now give theoretical justification of the verification approach and explains the underlying logical machinery. First, Sections 3.1 and 3.2 briefly discuss the syntax and semantics of process algebraic models and programs, respectively. Then Section 3.3 presents the program logic as a concurrent separation logic with assertions that allow to specify program behaviour as a process algebraic model. Section 3.4 formally introduces and discusses the proof rules. Finally, Section 3.5 discusses soundness of the approach. All these components have been fully formalised in Coq, including the soundness proof of the logic. Section 4 elaborates on the Coq development of the meta-theory, as well as on tool support, developed for the VerCors concurrency verifier.

Definition 1 (Processes).
e ∈ ProcExpr : Clarifying the different connectives and constructs, ε is the empty process, which has no behaviour. The δ process is the deadlocked process which neither progresses nor terminates. Processes of the form a(e) are actions, which model the basic, observable (shared-memory) system behaviours. Actions are parameterised by data, in the form of expressions e. The process P · Q is the sequential composition of P and Q, whereas P + Q is their non-deterministic choice. The parallel composition of processes P and Q is written P Q. The process P Q is the left-merge of P and Q, which is similar in spirit to parallel composition, however insists that the left-most process P proceeds first. The left-merge is an auxiliary connective commonly used to axiomatise parallel composition [31], by having P Q = P Q + Q P. The process Σ x P is the infinite summation P[x/v 0 ] + P[x/v 1 ] + · · · over all values v 0 , v 1 , ... ∈ Val. Any summation Σ x P is a binder for the summation variable x. In the remainder we assume without loss of generality that all variables bound by summation are unique (since any such variables can be renamed to unique ones if this is not yet the case). Sometimes Σ x 0 ,...,x n P is written to abbreviate Σ x 0 · · · Σ x n P. The conditional (guarded) process b : P behaves as P if the Boolean condition b holds, and otherwise behaves as δ. Finally, P * is the repetition, or iteration of P, and denotes a sequence of zero or more P's. The infinite iteration of P is derived to be P ω P * · δ. Finally, ?(b) is the assertive process, which is very similar to guarded processes: ?(b) is behaviourally equivalent to δ in case b does not hold. However, assertive processes have a special role in our approach: they are the main subject of process-algebraic analysis, as they encode the properties b to verify, as logical assertions. Moreover, they are a key component in connecting process-algebraic reasoning with deductive reasoning, as their properties can be relied upon in the deductive proofs of programs via the query b ghost command.

Action Contracts
The presented verification approach uses processes in the presence of data, which is implemented via action contracts. Action contracts consist of pre-and postconditions which we refer to as guards and effects, respectively, that logically describe the state changes that are imposed by the corresponding action. In the remainder of this article, each action is assumed to have an action contract assigned to it. Instead of defining syntax for writing these contracts, the following two functions are assumed for obtaining the pre-and postcondition of an action (from Act) and its data parameter (from ProcExpr), respectively.
Both these conditions are of type ProcCond, which is the domain of Boolean expressions over process-algebraic variables. Note that, since actions are parameterised by data (see Definition 1), both guard and effect take a second argument to account for the input parameter, which is of type ProcExpr-the type of arithmetic expressions over process-algebraic variables.
Here Act → ProcExpr → ProcCond should be read as Act → (ProcExpr → ProcCond) and interpreted as a function sequence (in the sense of currying). That is, it is the set of functions mapping Act to the set of functions mapping ProcExpr to ProcCond.

Free Variables and Substitution
A function fv e : ProcExpr → 2 ProcVar is used to determine the set of free process-algebraic variables in expressions as usual, and likewise for fv b (b) and fv P (P) for Boolean expressions b and processes P. We often omit the subscripts and simply write fv(·) whenever the context allows it. The definitions of fv e , fv b and fv P are mostly standard and thus deferred to [19]. Noteworthy however are: Substitution is written e [x/e] (and likewise for Boolean expressions and processes) and has a standard definition: replacing any occurrence of x inside e by the expression e. Noteworthy is that substitutions inside action processes a(e) do not affect the action contracts: a(e )[x/e] a(e [x/e]).

Operational Semantics
The denotational semantics of process-algebraic expressions [[·]] e : ProcExpr → ProcStore → Val and conditions [[·]] b : ProcCond → ProcStore → Bool is defined in the standard way, as total functions that evaluate to Val and Bool, resp. The set σ ∈ ProcStore ProcVar → Val is the domain of process stores, which are used to give an interpretation to all process-algebraic variables. The operational semantics of the process algebra language is expressed as a labelled binary small-step reduction relation α − −→ ⊆ ProcConf × ProcLabel × ProcConf over process configurations, defined as ProcConf Proc × ProcStore-pairs of processes and process stores. The labels α of the reduction rules are defined as follows: α ∈ ProcLabel ::= a(v) | assn. Transitions labelled a(v) are reductions of actions, whereas assn indicates reductions of assertions.
Before giving the reduction rules we first define a notion of successful termination P ↓ of processes P. Successful termination is only defined for processes that are well-formed. Any process P is defined to be well-formed if any action parameters (the e's in a(e)) and conditions (the b's in b : Q) occurring inside P are closed.
Intuitively, any process P can terminate successfully if P has the choice to have no further behaviour. This means that ε can always successfully terminate (↓-EPSILON), as it has no behaviour, while δ can never successfully terminate. Iteration P * can always successfully terminate (↓-ITER) as it may choose not to start iterating and thereby to behave as ε.
The small-step reduction rules of process configurations are given below. Likewise to the definition of successful termination, also these reduction rules require processes to be well-formed.
Most of the reduction rules are standard in spirit [32]. However, the handling of actions and their contracts make this process algebra language non-standard. More specifically, the non-standard −→-ACT reduction rule for action handling permits the state σ to change in any way, as long as these changes comply with the action contract. We will later use the −→-ACT rule to connect shared-memory updates in programs, to action contract-complying state changes on the process level.
Moreover, the notion of successful termination is used to define the reduction rule for sequential composition, −→-SEQ-R, which is standard in process algebra languages with ε [33]. (An alternative on the explicit use of successful termination is to introduce internal (τ-)transitions for the reductions of ε. However, this might make the remaining formalisation less elegant, for example by requiring a notion of weak bisimilarity, instead of the notion of strong bisimilarity that is introduced later in this section.)

Process-Algebraic Verification
Process-algebraic verification in our approach amounts to verifying that all reachable assertional processes ?(b) are always satisfied, which we are interested in so that the program logic can rely on the b's. Any process configuration (P, σ) fails to verify, or exhibits a fault, which we write (P, σ), if it can directly violate an assertion. Verifying a process, i.e., checking for fault absence, could for example be reduced to checking the µ-calculus formula [true * · ]false, e.g., using the mCRL2 model checker, where is modelled as an explicit fault state, meaning "no faults are every reachable".
Fault exhibition is defined inductively as follows.

Bisimulation
Our verification approach allows handling process-algebraic models up to (strong) bisimulation. Definition 7 (Bisimulation). Any binary relation R ⊆ Proc × Proc over processes is defined to be a bisimulation relation if, whenever P R Q, then: (1) P ↓ if and only if Q ↓. (2) (P, σ) if and only if (Q, σ), for any σ.
Any two processes P and Q are defined to be bisimilar, or bisimulation equivalent, written P ∼ = Q, if and only if there exists a bisimulation relation R such that P R Q. Bisimilarity expresses that both processes exhibit the same behaviour, in the sense that their action sequences describe the same state changes. Any bisimulation relation constitutes an equivalence relation. Furthermore, bisimilarity is a congruence for all process algebraic connectives.
Successful termination P ↓ can intuitively be understood as P being bisimilar to the process ε + P, that is, by having the choice to have no further behaviour. Proposition 1. If P ↓ then P ∼ = ε + P. Lemma 1. If P ∼ = Q and (P, σ), then (Q, σ). Figure 2 gives a list of bisimulation equivalences that hold for our process algebra language. Note that the left-merge connective is not strictly needed, in the sense that our approach does not rely on it, but can be used to prove for example that a(e) a (e ) is bisimilar to a(e) · a (e ) + a (e ) · a(e).

Programs
Our verification approach is formalised on the following simple concurrent pointer language, where X, Y, · · · ∈ Var are (program) variables.
This language is a variation of the language proposed by O'Hearn [24] and Brookes [23]. In particular, we extend their language with specification-only commands (code annotations) for handling process-algebraic models. These commands are coloured blue. Note that the blue colourings do not have any semantic meaning; they only indicate which language constructs are specification-only. Moreover, we interchangeably refer to commands also as programs.

Standard Language Constructs
The notation [E] stands for heap dereferencing, where E is an expression whose evaluation determines the heap location to dereference. The commands X := [E] and [E] := E denote heap reading and writing: they read from, and write to, the heap at location E, respectively. Moreover, X := alloc E allocates a free heap location and writes the value represented by E to it, whereas dispose E deallocates the heap location at E.
Regarding concurrency, the command C 1 C 2 is the statically-scoped parallel composition of C 1 and C 2 and expresses their concurrent execution. In the sequel, we sometimes refer to commands that are put in parallel as different threads; for example C 1 and C 2 in the above. Moreover, atomic C expresses a statically-scoped lock: it represents the atomic execution of C, that is, without interference of other threads. The command inatom C represents partially executed atomic programs: ones that are currently being executed, where C is the remaining program that still has to be executed atomically. Such commands are sometimes referred to as "runtime syntax", as they are not written by users of the language, but are instead an artefact of program execution.

Specification-Only Constructs
The instructions that are displayed in blue are the specification-only language constructs, for handling process-algebraic models in the logic. These instructions are ignored during regular program execution and are essentially handled as if they were code comments.
Specification-wise, X := process (λx.P)(E) over Π initialises a new process-algebraic model P in the proof system that takes a single input argument named x, namely (the evaluation of) the expression E. This model is used (1) as a specification of how a particular region of shared memory, specified by Π, is allowed to evolve over time; and (2) to support reasoning over the model to indirectly prove properties of how the heap evolves. The Π component is an abstraction binder, which is also defined in Definition 8 and is used to connect process-algebraic variables to heap locations in the program. In particular, the abstraction binders make the connections/links between process-algebraic state and shared-memory program state (that is, heap locations). In the sequel, we often use abstraction binders as if they were finite partial mappings, Π : ProcVar fin Expr, from process-algebraic variables to the expressions whose evaluation determine the corresponding heap location. Finally, the variable X identifies the process-algebraic model after initialisation.
The command finish E is used to finalise the process-algebraic model identified by E in the logic, given that it can successfully terminate. Finalisation is later explained in more detail, in Section 3.4. This language is a variation of the language proposed by O'Hearn [24] and Brookes [23]. In particular, we extend their language with specification-only commands (code annotations) for handling process-algebraic models. These commands are coloured blue. Note that the blue colourings do not have any semantic meaning; they only indicate which language constructs are specification-only. Moreover, we interchangeably refer to commands also as programs.

Standard Language Constructs
The notation [E] stands for heap dereferencing, where E is an expression whose evaluation determines the heap location to dereference. The commands X := [E] and [E] := E denote heap reading and writing: they read from, and write to, the heap at location E, respectively. Moreover, X := alloc E allocates a free heap location and writes the value represented by E to it, whereas dispose E deallocates the heap location at E.
Regarding concurrency, the command C 1 C 2 is the statically-scoped parallel composition of C 1 and C 2 and expresses their concurrent execution. In the sequel, we sometimes refer to commands that are put in parallel as different threads; for example C 1 and C 2 in the above. Moreover, atomic C expresses a statically-scoped lock: it represents the atomic execution of C, that is, without interference of other threads. The command inatom C represents partially executed atomic programs: ones that are currently being executed, where C is the remaining program that still has to be executed atomically. Such commands are sometimes referred to as "runtime syntax", as they are not written by users of the language, but are instead an artefact of program execution.

Specification-Only Constructs
The instructions that are displayed in blue are the specification-only language constructs, for handling process-algebraic models in the logic. These instructions are ignored during regular program execution and are essentially handled as if they were code comments.
Specification-wise, X := process (λx.P)(E) over Π initialises a new process-algebraic model P in the proof system that takes a single input argument named x, namely (the evaluation of) the expression E. This model is used (1) as a specification of how a particular region of shared memory, specified by Π, is allowed to evolve over time; and (2) to support reasoning over the model to indirectly prove properties of how the heap evolves. The Π component is an abstraction binder, which is also defined in Definition 8 and is used to connect process-algebraic variables to heap locations in the program. In particular, the abstraction binders make the connections/links between process-algebraic state and shared-memory program state (that is, heap locations). In the sequel, we often use abstraction binders as if they were finite partial mappings, Π : ProcVar fin Expr, from process-algebraic variables to the expressions whose evaluation determine the corresponding heap location. Finally, the variable X identifies the process-algebraic model after initialisation.
The command finish E is used to finalise the process-algebraic model identified by E in the logic, given that it can successfully terminate. Finalisation is later explained in more detail, in Section 3.4.
The specification command action E a(E ) do C is used to link the execution of programs with the execution of process-algebraic models. More specifically, it executes the program C in the context of the model identified by E, as the process-algebraic action a that takes (the evaluation of) E as an input argument. The soundness argument of the program logic establishes a refinement relation between programs and their models, and this relation is established by synchronising program execution with process execution, with help of these action blocks.
The inact C command denotes a partially executed action program; one that still has to execute C. Likewise to inatom, this command can only occur during runtime and is not written by users.
Lastly, query E is used to connect process-algebraic reasoning to deductive reasoning: it allows the deductive proof of the program to rely on (or assume) properties that are proven to hold (or guaranteed) on the process-algebraic model identified by E, via process-algebraic analysis. These are the properties that are encoded as assertions ?(·) in this model. Of course, this would require linking process-algebraic state to program state, which we come to later, in Sections 3.3 and 3.4.

Free Variables and Substitution
We use the standard (overloaded) notations FV(E), FV(B), FV(Π) and FV(C) to refer to the set of free program variables in the given (Boolean) expression E and B, abstraction binder Π, and command C, respectively. Moreover, the notation E[X/E ] denotes the substitution of the program variable X for the expression E inside E; and likewise for Boolean expressions, abstraction binders, and commands. The full definitions of FV(·) and (·)[X/E] are mostly standard, and therefore deferred to [19].

User Programs
As just discussed, our simple programming language contains runtime syntax-instructions that are not written by users but are only introduced during runtime. Commands that are free of such runtime constructs are called user commands.
Definition 9 (User commands). Any command C is defined to be a user command, denoted user(C), if C does not contain sub-commands of the forms inatom C and inact C , for any command C .

Wellformedness
Moreover, our verification approach only applies to well-formed commands. Notably, our technique requires that, for any program of the form action _ do C and inact C, the inner action program C only contains a subcategory of commands, excluding atomic commands and specification-only constructs, in particular nested action blocks. The latter is needed since actions must be atomically observable by environmental threads. This restriction is captured by the following definition.
Definition 10 (Basic programs, well-formed programs). Any command C is defined to be basic, denoted basic(C), if C does not contain any atomic sub-programs, i.e., atomic or inatom, nor specification-specific language constructs, i.e., process, action, inact, finish, or query.
A command C is defined to be well-formed, denoted wf(C), if, for any command action _ do C or inact C that occurs in C it holds that basic(C ).  The operational semantics of programs is defined in terms of a binary small-step reduction relation ⊆ Conf × Conf between program configurations. A program configuration C = (C, h, s) ∈ Conf Cmd × Heap × Store is a triple, consisting of a command C as well as a heap h that models shared memory, and a store s ∈ Store that models thread-local memory. Any program configuration of the form (skip, h, s) is defined to be final or terminated. Heaps h ∈ Heap Val fin Val are defined to be finite partial mappings from values to values. Heap locations are themselves values, so that they can be assigned to, and read from, local variables, and thus be handled as any value. The function dom : Definition 11 (Small-step operational semantics of programs).
Appl. Sci. 2020, xx, 5 13 of 48 Cmd × Heap × Store is a triple, consisting of a command C as well as a heap h that models shared memory, and a store s ∈ Store that models thread-local memory. Any program configuration of the form (skip, h, s) is defined to be final or terminated. Heaps h ∈ Heap Val fin Val are defined to be finite partial mappings from values to values. Heap locations are themselves values, so that they can be assigned to, and read from, local variables, and thus be handled as any value. The function dom : Definition 11 (Small-step operational semantics of programs). -ASSIGN Most of the transition rules are standard; see for example [34]. The update notation s[X → v] defines a store that is equal to s, except that X is mapped to v. A similar notation is used for heaps, namely h[v 1 → v 2 ]. Moreover, the notation h \ v denotes the removal of the entry at v in h.
An interesting aspect of the operational semantics is that atomic programs are executed using a small-step reduction strategy (via -INATOM-STEP and -INATOM-SKIP), rather than a big-step execution, which is more customary. This is done for technical reasons: it simplifies the establishment of a simulation/refinement between programs and their models. Consequently, we use a notion of a locked program to define the transition rules for atomic programs. Any command C is said to be (globally) locked if C executes an atomic program, i.e., if C has inatom C as a subprogram for some C .
Definition 12 (Locked programs). Any command C is locked if locked(C) holds, where locked ⊂ Cmd is defined as follows, by structural recursion on C: The rules -PAR-L and -PAR-R for parallel composition allow a thread to make an execution step only if the other thread is not locked, thereby preventing thread interference while executing atomic programs. One might ask whether this handling of locks could not potentially lead to deadlock scenarios, for example by encountering configurations (C 1 C 2 , h, s) during runtime for which both locked(C 1 ) and locked(C 2 ) hold. However, we will later see and prove that no such deadlocks can be reached, given that one starts with an initial configuration that contains a user program.
Furthermore, the specification-only language constructs do not affect the state of the program (not the heap nor the store) and are essentially handled as if they were comments. Notice however, that commands of the form action _ do C are first reduced to inact C before C is being executed. This is done for technical reasons, as this makes it more convenient to later establish a simulation relation between execution steps of programs and processes.
The semantics of programs has the following preservation properties.

Fault Semantics
Apart from an operational semantics, we also define a fault semantics for programs [35] that classifies runtime errors that may occur during program execution. Its definition uses two auxiliary functions, acc(C, s) and writes(C, s), for obtaining the set of heap locations that can be accessed or written-to, respectively, in a next reduction step of C. Their definitions are deferred to [19] as well, as they are quite lengthy and not essential for understanding the definition of the fault semantics.
The fault semantics of program configurations C is expressed as a predicate (C) that is inductively defined as follows.

Definition 13 (Fault semantics of programs).
-READ Intuitively, a program configuration exhibits a fault if it (1) accesses unallocated memory, or (2) is deadlocked, or (3) allows performing a data-race.
More specifically, -READ expresses that heap reading X := [E] faults if the heap location at E is unoccupied. For the same reason, also heap writing ( -WRITE) and heap deallocation ( -DISPOSE) may fault. The -PAR-L rule expresses that any parallel program C 1 C 2 can fault if C 1 can fault, given that C 2 is not locked, or the other way around ( -PAR-R covers the other direction). Program configurations that hold multiple global locks are also considered to be faulting, by -DEADLOCK. Finally, the fault semantics encodes the definition of a data-race, via -RACE-1 and -RACE-2. To clarify, any configuration (C, h, s) exhibits a data-race if C has (at least) two threads that can both access a common location in h in the next reduction step, where at least one of these accesses is a write.
We will later see that the soundness argument of our program logic covers that verified programs are free of faults. More specifically, we will prove that, for any program C for which a proof can be derived, we have that C is fault-free with respect to any heap h and store s that satisfy C's precondition, and moreover, that every configuration that is reachable from (C, h, s) is also fault-free.
Finally, to show that the operational semantics of programs is coherent with respect to faults, we prove that the operational semantics is progressive for all non-faulting program configurations.
Theorem 1 (Progress of ). For any program configuration C for which ¬ (C) holds, either C is final, or there exists a configuration C such that C C .

Assertions
The assertion language of our verification approach is defined by the following grammar.

Definition 14 (Assertions).
t ∈ PointsToType ::= std | proc | act P, Q, R, · · · ∈ Assn ::= B | ∀X.P | ∃X.P | P ∨ Q | P * Q | * i∈I P Assertions can be built from plain Boolean expressions B, and may contain several standard connectives from predicate logic: universal and existential quantifiers, and disjunction. Moreover, logical conjunction (∧) is replaced by the separating conjunction * from Concurrent Separation Logic (CSL). The * i∈I P i connective is the iterated separating conjunction, with I a finite set that represents P 0 * · · · * P n , given that I = {0, . . . , n}. The − * connective is known as the magic wand and is used to describe hypothetical judgments, much like the logical implication from predicate logic.
Apart from these standard CSL connectives, the assertion language contains three different heap ownership predicates π − → t , with π ∈ Q a rational number that represents a fractional permission, and t the heap ownership type, as well as an ownership predicate Proc π for program abstractions. Finally P ≈ Q intuitively means that P and Q are bisimilar processes with respect to the current state.
The definitions of free variables FV(P ) of assertions P, and substitution P [X/E] in P, are the standard ones and are therefore deferred to [19]. Assertions that are free of π − → t and Proc π predicates are called pure. Any assertion that is not pure is said to be spatial.

Heap Ownership
The assertion E 1 π − → t E 2 is the heap ownership assertion and expresses that the heap contains the value represented by the expression E 2 at heap location E 1 . Moreover, π and t together determine the access rights to this heap location. In more detail, depending on the ownership type t, the π − → t ownership predicates express different access rights to the associated heap location: • Standard heap ownership. E 1 π − → std E 2 is the standard heap ownership predicate from (intuitionistic) separation logic that provides read-access whenever 0 < π < 1, and write-access in case π = 1. Moreover, the subscript std indicates that the associated heap location E 1 is not bound to any process-algebraic model. We say that a heap location v ∈ Val is bound by, or subject to, a program abstraction, if there is an active program abstraction with a binder Π that contains a mapping to v, that is, v ∈ dom(Π).
• Process heap ownership. E π − → proc E is the process heap ownership predicate, which indicates that the heap location at E is bound by an active process-algebraic abstraction, but in a purely read-only manner. More precisely, π − → proc assertions exclusively grant read-access, even in case π = 1. • Action heap ownership. E π − → act E is the action heap ownership predicate, which indicates that the heap location E is bound by an active process-algebraic model, and is used in the context of an action block, in a read/write manner.
Observe that action points-to assertions π − → act essentially give the same access rights as π − → std assertions. Nevertheless, they are both needed, to be able to distinguish between bound and unbound heap locations in the logic. For example, the program logic must not allow to deallocate memory that is currently bound to (protected by) an active process-algebraic model, as this would be unsound.
Moreover, even though π − → proc predicates never grant write access, we will later see that the proof system allows π − → proc predicates to be upgraded to π − → act inside action blocks, and π − → act again provides write access when π = 1. More precisely, E 1 − → proc E predicates grant the capability to regain write access to E, in the context of an action program. This system of upgrading enforces that all modifications to E happen in the context of action E abstr a(E abstr ) do C commands, so that the modifications are protected and can be recorded by the program abstraction identified by E abstr , as the action a.
In addition to these three heap ownership predicates, we derive a fourth such predicate, called the process-action heap ownership predicate. This ownership predicate is equivalent to π − → act only if π denotes write access, and otherwise it is equivalent to π − → proc .
This derived predicate is for later use, in the proof system of our program logic. Finally, the notation E π − → t − is sometimes used as shorthand for ∃X.E π − → t X, where X ∈ FV(E).

Process Ownership
The Proc π (E, P, Π) assertion expresses ownership of a program abstraction that is identified by E, where the abstraction is represented by the process P. Ownership in this sense means that the thread has knowledge of the existence of the process-algebraic model P, as well as the right to execute as prescribed by this model. The mapping Π connects the abstract model to the concrete program by mapping the process-algebraic variables in the abstraction to heap locations in the program, as discussed before. And last, the fractional permission π is needed to implement the ownership system of program models. Fractional permissions are only used here to be able to reconstruct the full Proc 1 predicate. We shall later see that Proc π predicates can be split and merged along π and parallel compositions inside P, and be consumed in the proof system by action programs.
Even though reasoning about process-algebraic models is done purely on the level of process-algebraic state, in the program logic it is allowed to mix program state with process-algebraic state. This is indicated by the tilde above the P, which means that P can have both program variables and process-algebraic variables. Such processes are called hybrid processes and are defined as follows.
These hybrid processes thus allow mixing process-algebraic reasoning with deductive reasoning using our program logic. The function fv( P) is used for obtaining the set of free process-algebraic variables in P, and FV( P) for obtaining all free program variables in P (and likewise for E and B).
We shall later see that the program logic allows replaces processes P inside Proc π (E, P, Π) predicates by bisimilar ones. However, note that one cannot use the standard notion of bisimilarity as defined in Definition 7 for this in case P has any program variables occurring freely in it. To resolve this, we include a relation P ≈ Q in the assertion language, stating that P and Q are bisimilar while taking into account any (pure) information that is available from the context. This is further clarified in Section 3.3.7, after we discussed the models of the logic.

Models of the Program Logic
Before Section 3.3.7 discusses the semantics of assertions, this section first introduces permission heaps and process maps, that form the basis for the models of our concurrent separation logic. Permission heaps extend ordinary program heaps (i.e., Heap) to capture the three different types t of heap ownership, whereas process maps capture the state and ownership of process-algebraic abstractions.
Let us start by introducing fractional permissions, which are used in the definitions of both permission heaps and process maps.

Fractional Permissions
In the assertion language, all heap/process ownership predicates have an associated rational number π ∈ Q. There are used to express the "amount" of ownership that is available to the corresponding heap location or program model.
We define a rational number π to be a (Boyland) fractional permission in case π ∈ (0, 1] Q [36]. The original work of Boyland uses fractional permissions to distinguish between write access (π = 1) and read access (0 < π < 1) to some shared resource. However, in our work this is slightly different, since the fractional access permissions π annotated to π − → proc predicates never provide write access. To conveniently handle fractional permissions, we define basic notions of validity (valid Q ) and disjointness (⊥ Q ) of rational numbers, as follows.

Definition 17 (Permission validity, Permission disjointness).
The predicate valid Q : Q → Prop determines whether the given rational number is within the range (0, 1] Q , that is, is a valid Boyland fractional permission. (Here Prop is the sort of propositions.) The binary relation ⊥ Q : Q → Q → Prop determines disjointness of two rationals. Disjoint rational numbers do not overlap, in the sense that both operands are fractional permissions, as well as their addition. Lemma 4. valid Q and ⊥ Q satisfy the following properties.

Permission Heaps
The models of our program logic use permission heaps to give a semantic meaning to heap ownership. Permission heaps and their heap cells are defined as follows, and are slightly richer than ordinary program heaps (Heap) to be able to administer the access permissions and the different ownership types.
Permission heaps ph are defined to be total functions from values (representing heap locations) to permission heap cells, hc, which in turn are inductively defined to be one of the following: • free, which is an unoccupied heap cell.
• v π std , which is a standard heap cell that stores the value v ∈ Val. Standard heap cells are the models of the standard heap ownership predicates, π − → std .
• v π proc , which is a process heap cell that stores the value v. These are used as models of the π − → proc ownership predicates.
• v 1 , v 2 π act , which is an action heap cell that stores the value v 1 . Action heap cells are used as the models for the π − → act predicates. Moreover, action heap cells store a second value v 2 . This extra value is maintained for technical reasons, to help in establishing soundness of the program logic. The value v 2 is referred to as a snapshot value: a copy of the original value stored by the heap cell, that is made when an action block was entered.
• inv, which is an invalid, or corrupted, permission heap cell.
Note that, unlike program heaps, permission heaps are defined to be total functions, where the heap cells have an explicit notion of being free. This is done to give permission heaps and their cells nicer algebraic properties. The unit permission heap is defined to be 1 ph λv ∈ Val . free, containing free at every entry. Furthermore, permission heap cells also have an explicit notion of being invalid. Invalid heap cells inv represent the erroneous result of composing two incompatible heap cells.
We now define several operations on permission heaps. Validity. Any permission heap ph is defined to be valid if the permissions of all ph's heap cells are valid, where free is always valid and inv is never valid.

Definition 19 (Validity of permission heaps).
A permission heap ph is defined to be valid, written valid ph (ph), if valid hc (ph(v)) holds for every v ∈ Val, where the valid hc predicate is defined as follows.
valid hc (hc) Disjointness. Two permission heaps ph 1 and ph 2 are disjoint if all their heap cells are pairwise compatible and their underlying permissions are disjoint.
Definition 20 (Disjointness of permission heaps). Two permission heaps, ph 1 and ph 2 , are disjoint, denoted ph 1 ⊥ ph ph 2 , if ph 1 (v) ⊥ hc ph 2 (v) holds for every v ∈ Val, where the ⊥ hc relation is defined as follows.
false otherwise Disjoint union. The following operation defines the disjoint union (i.e., the composition) of two permission heaps.
Definition 21 (Disjoint union of permission heaps). The disjoint union ph 1 ph ph 2 of any two permission heaps ph 1 , ph 2 is defined to be the permission heap λv ∈ Val . ph 1 (v) hc ph 2 (v), with hc defined as follows.
Note that hc only gives a non-corrupted entry when applied to two compatible heap cells. Furthermore, free is neutral with respect to hc while inv is absorbing.
Below are the most important properties of validity, disjointness and disjoint union.
If ph ph 1 ph = ph.

Process Maps
The models of the logic also use process maps in addition to permission heaps, to give a semantic meaning to process ownership predicates Proc π in the logic. Process maps and their entries are defined as follows, where binders Λ are finite partial mappings from process variables to heap locations (i.e., values). These binders are the models for the abstraction binders Π defined earlier in Definition 8. (Process map entries, process maps, binders).

Definition 22
Process maps are total mappings from values (identifying program abstractions) to process map entries, which are, in turn, inductively defined to one of the following three elements: • free, which models unoccupied or free entries in pm.
• P, Λ π , which is an occupied process map entry. These are used as models for the Proc π (E, P, Π) assertions, where E identifies the process map entry in pm, and the binder Λ is a model for Π.

•
inv, which denotes an invalid, or corrupted, process map entry.
Likewise to permission heaps, process maps are defined as total functions with entries that can explicitly be free or invalid, as this provides desirable algebraic properties. Corrupted entries represent the erroneous result of taking the disjoint union of two incompatible, non-disjoint entries. The unit process map is defined to be 1 pm λv ∈ Val . free, containing free at every entry.
We now define several operations and relations on process maps that are analogous to the operations defined earlier for permission heaps, starting with bisimilarity.
Bisimilarity. Any two process maps are said to be bisimilar, if all their entries are equal point-wise, or contain occupied entries with process components that are bisimilar. Definition 23 (Process map bisimilarity). Two process maps pm 1 and pm 2 are defined to be bisimilar, denoted pm 1 ∼ =pm pm 2 , if pm 1 (v) ∼ =mc pm 2 (v) for every v ∈ Val, with the relation ∼ =mc defined as follows.
Both ∼ =pm and ∼ =mc are equivalence relations. A notion of bisimilarity of process maps is needed in addition to ordinary equality, since for example disjoint union of process maps is not associative nor commutative with respect to ordinary equality, as opposed to bisimilarity. Moreover, we will later see that the program logic always allows replacing processes P inside Proc π (X, P, Π) predicates by bisimilar ones, as discussed earlier. But to handle such replacements at the semantic level, we allow process maps and their entries to be handled up to ∼ =pm and ∼ =mc, respectively.
Validity. Any process map pm is said to be valid intuitively if none of pm's entries are corrupt and all occupied entries of pm hold a valid associated fractional permission.
Definition 24 (Process map validity). Any process map pm is defined to be valid, denoted valid pm (pm), if valid mc (pm(v)) holds for every v ∈ Val, with the valid mc : ProcMapEntry → Prop predicate defined as follows.
valid mc (me) It is not difficult to see that 1 pm is trivially valid, and that bisimilarity is validity-preserving, i.e., valid pm (pm 1 ) and pm 1 ∼ =pm pm 2 implies valid pm (pm 2 ) for every pm 1 and pm 2 ; and likewise for valid mc . Disjointness. Two process maps are said to be disjoint if none of their entries are corrupt, and all fractional permissions of their entries are point-wise disjoint, as captured by the following definition.
Definition 25 (Process map disjointness). Any two process maps pm 1 and pm 2 are defined to be disjoint, denoted pm 1 ⊥ pm pm 2 , if pm 1 (v) ⊥ mc pm 2 (v) for every v ∈ Val, with the ⊥ mc relation defined as follows.
The intuition of disjointness is that disjoint process maps can safely be composed without corrupting any of their entries. Disjointness is a symmetric relation and is a congruence with respect to bisimilarity, meaning that pm 1 ⊥ pm pm 2 and pm 1 ∼ =pm pm 1 and pm 2 ∼ =pm pm 2 implies pm 1 ⊥ pm pm 2 . Disjoint union. The following operation defines the disjoint union of two process map (entries).
Definition 26 (Disjoint union of process maps). The disjoint union of two process maps pm 1 and pm 2 is defined as pm 1 pm pm 2 ∀v . pm 1 (v) mc pm 2 (v), with mc defined as follows.
Likewise to disjoint union of permission heaps, the composition of incompatible process map entries produces a corrupted inv entry. The free entry is again neutral, whereas inv is absorbing (that is, composing inv with any entry results in inv). Disjoint union is a congruence with respect to bisimilarity, so that pm 1 ∼ =pm pm 2 and pm 1 ∼ =pm pm 2 implies pm 1 pm pm 1 ∼ =pm pm 2 pm pm 2 .
Lemma 6. (The analogous operations on process map entries have the exact same properties.) pm 1 pm pm 2 ∼ =pm pm 2 pm pm 1 .

Semantics of Assertions
Let us now define the semantic meaning of assertions. The semantics of assertions is defined in terms of a satisfaction relation ph, pm, s, g |= P stating that the assertion P is satisfied by the model (ph, pm, s, g). Its definition depends on an operation [[·]] : AbstrBinder → Store → Binder for evaluating abstraction binders Π = {x 0 → E 0 , . . . , x n → E n }, that is defined as follows.

Definition 27 (Abstraction binder evaluation).
[ Moreover, recall that hybrid processes may contain both program variables and process-algebraic variables. The semantics of assertions relies on a closure operation for "closing" processes with respect to any program variable occurring in it. More specifically, given any hybrid process P and store s, the s-closure of P, written P[s], is defined to be P[X/s(X)] X∈FV( P) , i.e., replacing every free program variable X in P by s(X). This operation is "closing" P in the sense that FV( P[s]) = ∅ and P[s] ∈ Proc.
Definition 28 (Semantics of assertions). The modelling relation ph, pm, s, g |= P is defined by structural recursion on P as follows.
ph, pm, s, g |= P 1 ∨ P 1 iff ph, pm, s, g |= P 1 ∨ ph, pm, s, g |= P 2 ph, pm, s, g |= P 1 * P 2 iff ∃ph 1 , ph 2 . ph 1 ⊥ ph ph 2 ∧ ph 1 ph ph 2 = ph ∧ ∃pm 1 , pm 2 . pm 1 ⊥ pm pm 2 ∧ pm 1 pm pm 2 ∼ =pm pm ∧ ph 1 , pm 1 , s, g |= P 1 ∧ ph 2 , pm 2 , s, g |= P 2 ph, pm, s, g |= * i∈I P i iff ph, pm, s, g |= P i 0 * · · · * P i n for I = {i 0 , . . . , i n } ph, pm, s, g |= P 1 − * P 2 iff ∀ph , pm . (ph ⊥ ph ph ∧ pm ⊥ pm pm ∧ ph , pm , s, g |= P 1 ) =⇒ ph ph ph , pm pm pm , s, g |= P 2 ph, pm, s, g |= E 1 As usual, any separating conjunction P 1 * P 2 is satisfied by a model (ph, pm, s, g) if that model can both be split along ph and pm into two disjoint models, such that one satisfies P 1 and the other satisfies P 2 . The semantic meaning of iterated separating conjunctions can be expressed simply in terms of the interpretation of the binary separating conjunction. Magic wands P 1 − * P 2 are satisfied by a model if, for any disjoint extension of that model satisfying P 1 , the extended model satisfies P 2 .
Moving to the non-standard connectives; heap ownership assertions E π − → t E are satisfied if the permission heap holds an entry at location E that matches with the ownership type t, with an associated fractional permission that is at least π. Process ownership assertions Proc π (E, P, Π) are satisfied if the process map holds a matching entry at the position described by E, with a fractional permission at least π, and a process that at least includes all the behaviours of the process P[s]. Finally, P ≈ Q is satisfied if P and Q are bisimilar with respect to the current state. To give an example of the use of ≈, consider the assertion Proc π (E, 0 < X : P, Π) * X = 2. One might wish to replace 0 < X : P with P, considering that X = 2. But since 0 < X : P is a process that includes program variables (namely X), one can not immediately deduce that it is bisimilar to P according to Definition 7. However, we do have that 0 < X : P ≈ P * X = 2, since for every model (ph, pm, s, g) satisfying this assertion it holds that s(X) = 2. We shall later give entailment rules that allow such context-dependent bisimulation equivalences to be used to simplify processes inside Proc π ownership predicates. Lemma 7. The |= modelling relation satisfies the following properties: 1.
ph, pm, s, g |= P and pm ∼ =pm pm implies ph, pm , s, g |= P.

2.
If ph, pm, s, g |= P, then for any ph and pm such that ph ⊥ ph ph and pm ⊥ pm pm it holds that ph ph ph , pm pm pm , s, g |= P.
Lemma 7.1 is essential for allowing replacing process-algebraic abstractions by bisimilar ones inside the program logic. Lemma 7.2 expresses monotonicity, and states that adding resources does not invalidate the satisfiability of any assertion (i.e., adding more resources makes the assertion "more true"). This is a key property of intuitionistic separation logic and is necessary for proving soundness of the weakening rule, which we introduce later in Section 3.4.1.

Let the denotation [[P ]]
{(ph, pm, s, g) | (ph, pm, s, g) |= P } be the set of all models that are satisfied by the assertion P. Given any two assertions P and Q, the assertion P is defined to semantically entail Q, denoted P |= Q, if every model of P is also a model of Q, that is, Semantic entailment is thus a preorder and a congruence for all connectives of the assertion language.

Proof System
This section introduces the proof system of our model-based verification technique, which consists of structural proof rules (Section 3.4.1) as well as Hoare proof rules (Section 3.4.2). This proof system essentially extends the CSL of [34] by adding permission accounting [36,37] and machinery for handling process-algebraic program abstractions. Figure 3 presents the structural rules of the program logic. The notation P Q is a shorthand for both P Q and Q P, and indicates that the rule can be used in both directions.

Entailment Rules
The rules for the standard connectives are mostly as expected. PLAIN-DUPL expresses that plain expressions can freely be duplicated, whereas * -PLAIN shows that * has the same meaning as ∧ in the case of plain assertions. The rule * -WEAK shows that our concurrent separation logic is affine (intuitionistic) by allowing to forget about resources. The rules * -ASSOC and * -COMM express that the separating conjunction is associative and commutative, respectively, whereas * -TRUE allows composing any resource with true. The rule true-INTRO is the introduction rule for true, while false-ELIM is the elimination rule for false stating that anything can be derived from falsehood. The − * -INTRO and − * -ELIM rules show that magic wands can be used similarly to the modus ponens inference rule of propositional logic, with respect to * . The rules ∀-INTRO, ∀-ELIM, ∃-INTRO and ∃-ELIM are the standard introduction and elimination rules for universal and existential quantifiers. ITER-SPLIT-MERGE enables splitting and merging iterated separating conjunctions along the associated (finite) index set.
Clarifying the rules for handling heap ownership; − →-SPLIT-MERGE expresses that heap ownership predicates π − → t of any type t may be split (in the left-to-right direction) as well as be merged (right-to-left) along π. Note however, that multiple points-to predicates for the same heap location may only co-exist if they have the same ownership type, as indicated by the − →-INCOMPATIBLE rule. Any heap ownership assertion with an invalid fractional permission associated to it entails false by − →-INVALID Furthermore, the * -PROCACT-SPLIT-MERGE inference rule states that iterated procact heap ownership predicates can be split into disjoint iterated proc and act predicates, or be merged into one such iteration.

Standard connectives (excerpt)
Proc π 1 +π 2 (X, P 1 P 2 , Π) Proc π 1 (X, P 1 , Π) * Proc π 2 (X, P 2 , Π) Moving to the entailment rules for handling process-algebraic abstractions; Proc-∼ = allows replacing any process by one that is bisimilar in the current context. The rules ≈-REFL, ≈-SYMM and ≈-TRANS show that context-dependent bisimilarity forms an equivalence relation in the logic with respect to separating conjunction, while ≈-CONG-•, ≈-CONG-SUM, ≈-CONG-COND and ≈-CONG-ITER together show that ≈ is a congruence relation in the logic for all process-algebraic connectives. The rules ≈-COND-TRUE and ≈-COND-FALSE allows eliminating conditionals in any processes (together with the Proc-∼ = rule that is). Moreover, ≈-SUM-ALT allows singling out a single choice out of a process-algebraic summation of choices. Notice here that one can pick any arbitrary program expression for singling out such a choice, which makes this rule particularly useful. Similarly to heap ownership, any process ownership with an invalid fractional permission entails false by Proc-INVALID. The rule ≈-TERM again makes explicit the intuitive meaning of successful termination, matching Proposition 1. Finally, Proc-SPLIT-MERGE allows splitting and merging process ownership predicates in the same style as π − → t , to distribute parallel processes over parallel threads. Notably, by splitting a predicate Proc π 1 +π 2 (X, P 1 P 2 , Π) into two, both parts can be distributed over different concurrent threads in the program logic, so that thread i can establish that it executes as prescribed by its part Proc π i (X, P i , Π) of the abstract model. Afterwards, when the threads join again the remaining partial abstractions can be merged back into a single Proc π 1 +π 2 predicate. This system of splitting and merging thus provides a compositional, thread-modular way of verifying that programs meet their abstraction. The logical machinery of this is further discussed in Section 3.4.2.
Any deduction that can be derived using the rules of Figure 3 is sound in the standard sense: Theorem 2 (Soundness of the entailment rules). P Q implies P |= Q.

Program Judgments
We now define program judgments and give the Hoare rules of the program logic. Judgments of programs are sequents (quintuples) of the form Γ; R {P } C {Q}. The right-hand side is a traditional Hoare triple, whereas R is a resource invariant that captures resources available only to atomically executing programs (i.e., in executions that are free of thread interference), and Γ an environment in the style of interface specifications of [38]. These process environments have the following definition.

Definition 29 (Process environments).
Γ ∈ ProcEnv ::= ∅ | Γ, { B} P(x) That is, process environments are comma-separated sequences of pairs { B} P(x) of processes P and their precondition B, with x a placeholder variable for an input parameter that may occur freely in both P and B. Note that processes do not have postconditions here; if desired one could encode process Hoare triples op top of these pairs as { B pre } P(x) { B post } { B pre } (P(x) · ?( B post ))-by adding a trailing assertion. Moreover, even though process environments are given as sequences, they are used as if they were (finite) sets, as is customary, in the sense that the order of their pairs is unimportant.
Process environments contain the contracts of the process-algebraic models defined for the program under verification. In particular, they allow for assume-guarantee style reasoning: the proof system may assume validity of these contracts when dealing with process-algebraic models, since they must be guaranteed externally, for example via interactive theorem proving or model checking, e.g., using mCRL2 [25]. Validity of process contracts and process environments is defined as follows.

Definition 30 (Validity of process environments). Any pair { B} P(x) of a process P and its precondition B is defined to be
Any process environment Γ is defined to be valid if |= env Γ, which is a judgment inductively defined by the following two rules.
|= env Γ, { B} P(x) Figures 4 and 5 give the Hoare rules of the logic. The standard structural rules are essentially the same as the ones of classical CSL [34]. One minor difference is that HT-ATOMIC leaves true instead of "emp" after obtaining a resource invariant, since our logic is intuitionistic. (The assertion language of classical CSL contains an extra emp construct, for explicitly denoting that the heap is empty. In our intuitionistic version of the logic, resources are allowed to be thrown away using * -WEAK. As a consequence, assertions cannot express "precise" properties about the content of the heap, including emptiness of heaps.) Moreover, our assertion language does not contain the logical conjunction ∧ connective, but has separating conjunction instead.

Process-related proof rules
− → proc E i * Proc π (E, P, Π) * B assn Figure 5. The extended proof rules related to handling process-algebraic models.

Heap Ownership
The HT-READ rule states that reading from the heap is allowed with any type t of heap ownership π − → t , whereas heap writing (HT-WRITE) is only allowed with ownership predicates of type std or act. The HT-WRITE rule thus restricts π − → proc assertions to exclusively grant read-access to the associated location. We will in a moment see that the proof rule for action programs can upgrade E π − → proc E predicates to E π − → act E to regain write access to the heap location at E. This system of upgrading enforces that all modifications to E are captured by the program abstraction to which the heap location is subject to, inside an action block. The rule HT-ALLOC for heap allocation generates a new points-to predicate of type std, indicating that the allocated heap location is not (yet) subject to any program abstraction. Heap deallocation (HT-DISPOSE) requires a full standard ownership predicate for the associated heap location, thereby making sure that the deallocation does not break any bindings of active program abstractions, which would be unsound. Figure 5 gives the Hoare rules for introducing, eliminating and updating process-algebraic abstractions. The HT-PROC-INIT rule handles initialisation of an abstract model P with input parameter y, over a set of heap locations as specified by Π. This rule requires standard heap write ownership for any heap location that is to be bound by P according to Π, and these are converted to 1 − → proc . Moreover, Figure 5. The extended proof rules related to handling process-algebraic models.

Heap Ownership
The HT-READ rule states that reading from the heap is allowed with any type t of heap ownership π − → t , whereas heap writing (HT-WRITE) is only allowed with ownership predicates of type std or act. The HT-WRITE rule thus restricts π − → proc assertions to exclusively grant read-access to the associated location. We will in a moment see that the proof rule for action programs can upgrade E π − → proc E predicates to E π − → act E to regain write access to the heap location at E. This system of upgrading enforces that all modifications to E are captured by the program abstraction to which the heap location is subject to, inside an action block. The rule HT-ALLOC for heap allocation generates a new points-to predicate of type std, indicating that the allocated heap location is not (yet) subject to any program abstraction. Heap deallocation (HT-DISPOSE) requires a full standard ownership predicate for the associated heap location, thereby making sure that the deallocation does not break any bindings of active program abstractions, which would be unsound. Figure 5 gives the Hoare rules for introducing, eliminating and updating process-algebraic abstractions. The HT-PROC-INIT rule handles initialisation of an abstract model P with input parameter y, over a set of heap locations as specified by Π. This rule requires standard heap write ownership for any heap location that is to be bound by P according to Π, and these are converted to 1 − → proc . Moreover, HT-PROC-INIT requires that the precondition B of P holds, which is constructed from B by replacing all process variables x i by the values E i at the corresponding heap locations as specified by Π. (Here we slightly abuse notation for ease of presentation. In the proof rule, we write B[x i /E i ] ∀i∈I for converting a condition B to a condition over only program variables, by substituting all free process variables x i occurring in B by a program expression E i . However, in our Coq formalisation we of course have a special operation for such conversions.) A Proc 1 predicate with full permission is ensured, giving the current thread full ownership of the abstraction.

Process Ownership
The HT-PROC-UPDATE rule handles updates to program abstractions, by performing an action a(E ) in the context of an action E a(E ) do C program. This rule imposes four preconditions on handling action programs. First, a predicate of the form Proc π (E, a(E ) · P + Q, Π) is required for some π. In particular, the process component must be of the form a(E ) · P + Q and therewith allow performing the a action. After performing a the process will be reduced to P, and Q will be discarded as the choice is made not to follow execution as prescribed by Q. To get processes in the required format, the entailment rules in Figure 3 can be used together with the bisimulation equivalences given earlier in Figure 2. To give an example, processes of the form a(E ) · P can always be rewritten to a(E ) · P + δ to obtain the required choice. Second, π − → proc predicates are required for any heap location that is bound by Π. These points-to predicates are needed to resolve the guard and effect of a. Third, a's guard must indeed hold. (The notation guard a E is a shorthand for (guard a z)[z/E ] for some fresh z ∈ ProcVar, and likewise for effect a E .) And last, the remaining resource P must hold as well.
Among the premises of HT-PROC-UPDATE is a proof derivation for the sub-program C, in which all required π i − → proc predicates are "upgraded" to π i − → act and thereby regain write access when π i = 1. However, in case π i < 1 the upgrade does not give any additional privileges, since π i − → proc provides read-access just the same. We found that these unnecessary conversions complicate the soundness proof. To avoid unnecessary upgrades, we convert all affected π i − → proc predicates to π i − → procact instead, which simplifies the correctness proof. The HT-PROC-UPDATE rule ensures a process ownership predicate that holds the resulting process P after execution of a. In addition, updates to the heap are ensured that comply with the postconditions of the proof derivation of C.
HT-PROC-FINISH handles finalisation of process-algebraic models that can successfully terminate. A predicate Proc 1 (E, ε + P, Π) with full permission is required, implying that no other thread can have a fragment of the abstract model. The rule converts all bound 1 − → proc ownership predicates back to 1 − → std ownerships to indicate that these are no longer subject to the abstract model.
Lastly, HT-PROC-QUERY allows "querying" for properties B that are verified on the process algebra level. Recall that the main objective of process-algebraic analysis is to verify that all reachable assertions ?( B) hold. Observe that in this rule, the assertions may contain program variables in addition to process-algebraic variables, as these may have been introduced via summations (≈-SUM-ALT) or input parameters (HT-PROC-INIT). The soundness argument of the logic makes sure that the process-algebraic analysis can still be done fully on the process level (i.e., without relying on program state), meaning that this rule really makes a fusion between process-algebraic reasoning and deductive reasoning.

Soundness
This section defines the semantic meaning of program judgments and discuss the soundness proof of the program logic. This soundness proof has been mechanised using Coq, which was non-trivial and required substantial auxiliary definitions. This section discusses the most important auxiliary definitions and explains their use. For further proof details we refer to the Coq development [19].
The soundness theorem relates program judgments to the operational semantics of programs and boils down to the following: if (1) a proof Γ; R {P } C {Q} can be derived for any program C and (2) the contracts in Γ of all abstract models of C are satisfied (proven externally), then C executes safely for any number of computation steps. Execution safety in this sense also includes that C does not fault for any number of reduction steps with respect to the fault semantics of programs; see Definition 13.
Our definition of execution safety extends the well-known inductive definition of configuration safety of Vafeiadis [34] by adding machinery to handle process-algebraic abstractions. The most important extension is a simulation argument between concrete program executions (with respect to ) and the executions of all active models (with respect to α − −→). However, as the reduction steps of these two semantics do not directly correspond one-to-one, this simulation is established via an intermediate instrumented semantics referred to as the ghost operational semantics. This intermediate semantics is defined in Section 3.5.1 in terms of ghost transitions ghost that essentially define the lock-step execution of program transitions and the transitions α − −→ of their abstractions. Our definition of "executing safely for n execution steps" includes that all steps can be simulated by ghost steps, and vice versa, for n execution steps. Thus, the end-result is a refinement between programs and their abstractions.
In addition to establishing such refinements, our definition of execution safety must ensure that the HT-PROC-QUERY proof rule is sound. In other words, it must allow relying on any assertions embedded in the process-algebraic models in a sound manner, as these are (assumed to be) verified externally. To account for these assertions, the definition of execution safety needs to maintain the invariant that all active program abstractions preserve their execution safety as defined in Definition 5 for n execution steps, with respect to the current state of the program. (Recall that any process P is said to be safe according to this definition if P's assertions always hold.) The details of maintaining this invariant are discussed further in Section 3.5.3.
Finally, Section 3.5.4 formally defines process execution safety-the semantic meaning of program judgments-and presents the exact soundness statement.

Ghost Operational Semantics
To establish the refinements between programs and their abstractions, an intermediate semantics is used that administers the states of all active program abstractions. This intermediate semantics is referred to in the sequel as the ghost operational semantics. The ghost semantics is expressed as a transition relation ghost ⊆ GhostConf between ghost configurations G = (C, h, pm, s, g) ∈ GhostConf , which extend program configurations by two extra components, namely: • A process map pm ∈ ProcMap that is used to administer the state of all active (initialised, but not yet finalised) process-algebraic abstractions; and • An extra store g ∈ Store, referred to as a ghost store, as it is used to map variable names to process identifiers in the context of "ghost" instructions.
The ghost operational semantics uses two stores instead of one, to keep the administration of program data and specification-only (ghost) data strictly separated. Doing so eases establishing that variables referred to in ghost code do not interfere with regular program execution, and vice versa.
Ghost reductions essentially describe the lock-step execution of concrete programs ( steps) and their abstractions ( α − −→ steps). Figure 6 presents an excerpt of the transition rules. This excerpt only contains the reduction rules related to program abstraction; all other rules are essentially the same as those of , with the two extra configuration components simply carried over and left unchanged. Recall that the blue colourings are merely visual cues and do not have any special semantical meaning.
Clarifying the ghost reduction rules, ghost-PROC-INIT instantiates a new program abstraction and stores it in a free entry in pm. ghost-PROC-FINISH finalises program abstractions that are able to terminate successfully. The rules ghost-ACT-INIT, ghost-ACT-STEP and ghost-ACT-END handle the execution of action blocks. Before discussing these, first observe that the ghost semantics maintains an extra component m in inact m C commands, containing (ghost) metadata: extra runtime information about the process-algebraic model in whose context the program C is being executed. Concretely, ghost metadata m is defined as a quadruple m = (a, v, v , h) ∈ Act × Val × Val × Heap, consisting of: The label a of the action that is being executed; 2.
The input argument v for this action;

3.
The identifier v of the corresponding process-algebraic model in the process map, in which the action a is to be executed; and 4.
A copy h of the heap, made when the program started to execute the action block; that is, when the action program was reduced to inact by .

3.
The identifier v of the corresponding process-algebraic model in the process map, in which the action a is to be executed; and 4.
A copy h of the heap, made when the program started to execute the action block; that is, when the action program was reduced to inact by . The ghost-ACT-INIT reduction rule starts executing an action block by reducing it to an inact program, thereby assembling and attaching ghost metadata. In particular, a copy of the heap is made at this point, so that the ghost-ACT-END rule for finalising inact programs is able to access the original contents of the heap. This is needed to allow the abstraction to make a matching α − −→ step; in particular to determine the pre-state of such a step. To see how this works, first recall that the process-algebraic state of program abstractions are linked to concrete program state-entries in the heap-via the Λ binders maintained in process maps. Therefore, to be able to make an α − −→ step, the ghost-ACT-END rule first needs to construct process-algebraic state out of the current program state. This is done using the auxiliary function || · || : Binder → Heap → ProcStore referred to as the abstract state reification function.

Definition 31 (Abstract state reification).
The ghost-ACT-STEP rule allows making reductions in the context of inact programs. Finally ghost-QUERY handles reductions of assertions and synchronises any assn −−→ reductions on the process level with reductions of queries on the program level, with respect to the reified program state. The ghost-ACT-INIT reduction rule starts executing an action block by reducing it to an inact program, thereby assembling and attaching ghost metadata. In particular, a copy of the heap is made at this point, so that the ghost-ACT-END rule for finalising inact programs is able to access the original contents of the heap. This is needed to allow the abstraction to make a matching α − −→ step; in particular to determine the pre-state of such a step. To see how this works, first recall that the process-algebraic state of program abstractions are linked to concrete program state-entries in the heap-via the Λ binders maintained in process maps. Therefore, to be able to make an α − −→ step, the ghost-ACT-END rule first needs to construct process-algebraic state out of the current program state. This is done using the auxiliary function || · || : Binder → Heap → ProcStore referred to as the abstract state reification function.

Definition 31 (Abstract state reification).
The ghost-ACT-STEP rule allows making reductions in the context of inact programs. Finally ghost-QUERY handles reductions of assertions and synchronises any assn −−→ reductions on the process level with reductions of queries on the program level, with respect to the reified program state.

Faulting Ghost Configurations
In addition to faulting program configurations (Definition 13) we also define a fault semantics for ghost configurations. This ghost fault semantics is expressed in terms of a predicate ghost (G) over ghost configurations G. Figure 7 gives an excerpt of the rules. Only the rules related to specification-only constructs are shown. All other rules are essentially the same as those of Definition 13. We shall later show and prove properties that connect the two faulting semantics.

Faulting Ghost Configurations
In addition to faulting program configurations (Definition 13) we also define a fault semantics for ghost configurations. This ghost fault semantics is expressed in terms of a predicate ghost (G) over ghost configurations G. Figure 7 gives an excerpt of the rules. Only the rules related to specification-only constructs are shown. All other rules are essentially the same as those of Definition 13. We shall later show and prove properties that connect the two faulting semantics. Clarifying the ghost fault semantics; the initialisation of any process-algebraic model faults if there is no free entry available in pm (by ghost -PROC-FULL). The finalisation of program abstractions can fault if the corresponding entry in the process map is (1) either unoccupied or invalid ( ghost -PROC-FINISH-1), or (2) contains a process-algebraic abstraction that is unable to successfully terminate (by the rule ghost -PROC-FINISH-2). Reductions within action blocks inact m C may fault if (1) m does not refer to an abstraction ( ghost -ACT-SKIP-1), or (2) the abstraction relies on process variables that have an incorrect binding (by the rule ghost -ACT-SKIP-2), or (3) the process is not able to make a matching step ( ghost -ACT-SKIP-3), or (4) the subprogram C is able to fault (by ghost -ACT-STEP). Any query program can fault under similar conditions as those of action programs.
The ghost semantics enjoys the same progress property as the standard operational semantics.
Theorem 3 (Progress of ghost ). For any ghost configuration G for which ¬ ghost (G) holds, either G is final, or there exists a G such that G ghost G .
Moreover, it is quite straightforward to establish a forward simulation between and ghost . A matching backward simulation is ensured by the soundness argument of the program logic, as is customary for establishing refinements [39]. Clarifying the ghost fault semantics; the initialisation of any process-algebraic model faults if there is no free entry available in pm (by ghost -PROC-FULL). The finalisation of program abstractions can fault if the corresponding entry in the process map is (1) either unoccupied or invalid ( ghost -PROC-FINISH-1), or (2) contains a process-algebraic abstraction that is unable to successfully terminate (by the rule ghost -PROC-FINISH-2). Reductions within action blocks inact m C may fault if (1) m does not refer to an abstraction ( ghost -ACT-SKIP-1), or (2) the abstraction relies on process variables that have an incorrect binding (by the rule ghost -ACT-SKIP-2), or (3) the process is not able to make a matching step ( ghost -ACT-SKIP-3), or (4) the subprogram C is able to fault (by ghost -ACT-STEP). Any query program can fault under similar conditions as those of action programs.
The ghost semantics enjoys the same progress property as the standard operational semantics.
Theorem 3 (Progress of ghost ). For any ghost configuration G for which ¬ ghost (G) holds, either G is final, or there exists a G such that G ghost G .
Moreover, it is quite straightforward to establish a forward simulation between and ghost . A matching backward simulation is ensured by the soundness argument of the program logic, as is customary for establishing refinements [39].

Lemma 8 (Forward simulation).
The standard operational semantics and the fault semantics of programs are embedded in the ghost operational semantics and ghost fault semantics, respectively: 1.

2.
If (C, h, s), then also ghost (C, h, pm, s, g), for any pm and g.
The above theorem also shows that the ghost fault semantics extends . The soundness argument of the program logic establishes that verified programs do not fault with respect to ghost , and thus also do not fault with respect to by the above Lemma.

Preservation of Process Execution Safety
As already hinted upon in the preamble of this section, establishing soundness of the program logic requires maintaining an invariant stating that all active program abstractions retain their execution safety throughout program execution, with respect to Definitions 5 and 6. Since process maps are used to administer the status of all active program abstractions, we lift the notion of process configuration safety (Definition 5) to safety of process maps. Process map safety is expressed in terms of judgments of the form h |= pm pm stating that pm is safe if all process-algebraic models stored in pm execute safely with respect to Definition 5 together with a process store that is constructed (reified) from h.
where h |= mc mc is defined by case distinction on mc, so that Free process cells are always safe whereas corrupted entries inv are never safe. Moreover, both |= pm and |= mc are closed under bisimilarity of process maps and their entries, respectively. Lemma 9.

1.
If h |= pm pm and pm ∼ =pm pm , then h |= pm pm .

2.
If h |= mc me and me ∼ =mc me , then h |= mc me .
In a moment we will also define a notion of execution safety for commands. This notion of program execution safety maintains the aforementioned invariant that h |= pm pm always holds throughout program execution, where h and pm are constructed from the current state, at every execution step. This invariant is needed to establish soundness of the HT-PROC-QUERY proof rule.
However, one must be careful on how to exactly state this invariant, to allow re-establishing it after every computation step. In most cases re-establishing the invariant is straightforward. For example, h |= pm pm can be re-established after initialising a new program abstraction using the HT-PROC-INIT proof rule, by Definition 6 and by the structure of that proof rule. The invariant can also trivially be re-established after finalising an abstraction using HT-PROC-FINISH, as the abstraction is then no longer active and thereby removed from pm. However, computation steps that involve heap writing (i.e., handling of [E] := E programs) may be problematic, as illustrated below.
Technicality 1 (Potential problems due to heap writing). To see the potential problem, consider the following code snippet. The root of the problem is that the invariant should not necessarily have to hold during intermediate reduction steps while executing action programs, but only at the pre-and poststate of such programs. Program execution safety will solve this by making a snapshot of the heap every time an action program is being started on (likewise to ghost-ACT-INIT), and expressing the invariant over these snapshot heaps. Snapshots are recorded at the level of permission heaps, which already have the required structure to do this: action heap cells v 1 , v 2 π act allow storing snapshot values v 2 alongside "concrete" values v 1 . These snapshot values are used to construct snapshot heaps.

Definition 33 (Snapshot heaps). The snapshot of a permission heap is defined in terms of a total function
· snapshot : PermHeap → Heap, so that ph snapshot λv ∈ Val . ph(v) snapshot , with act for some v 1 and π undefined otherwise The snapshot heap ph snapshot of any permission heap ph only contains heap cells bound by process-algebraic models, and is constructed by taking the snapshot values of all ph's action heap cells. As we shall see in a moment, the final invariant maintained by program execution safety will be ph snapshot |= pm pm, where ph and pm are taken from the models of the program logic and represent the current state of the program. This invariant, combined with establishing a refinement between the program and its abstract models, provide sufficient means for proving soundness of the program logic.

Adequacy
This section defines program execution safety and uses it to define the semantic meaning of program judgments, from which the soundness theorem (i.e., adequacy of the logic) can be formulated. Program execution safety extends on the well-known notion of configuration safety of [34], by adding permission accounting, process-algebraic state, and the machinery introduced earlier in this section.
First, in order to help connect the models of the program logic to concrete program state, we define a concretisation function for permission heaps in the same style as snapshot heaps.
Definition 34 (Concretisation). Concretisation of permission heaps is defined as a total function · concr : PermHeap → Heap, so that ph concr λv ∈ Val . ph(v) concr , with ph(v) concr defined as act for some π and v 2 undefined otherwise Suppose that h |= pm pm holds on line 5. After computing line 6, the heap h holds the value −2 at location [[E ]]s. Moreover, the process map pm has not been changed, since the action program (lines 5-8) has not fully been executed yet. Nevertheless, h[[[E ]]s → −2] |= pm pm may now be violated, as the reset action can no longer be performed, since x = −2 after reification, while reset's guard requires x to be positive.
The root of the problem is that the invariant should not necessarily have to hold during intermediate reduction steps while executing action programs, but only at the pre-and poststate of such programs. Program execution safety will solve this by making a snapshot of the heap every time an action program is being started on (likewise to ghost-ACT-INIT), and expressing the invariant over these snapshot heaps. Snapshots are recorded at the level of permission heaps, which already have the required structure to do this: action heap cells v 1 , v 2 π act allow storing snapshot values v 2 alongside "concrete" values v 1 . These snapshot values are used to construct snapshot heaps.

Definition 33 (Snapshot heaps). The snapshot of a permission heap is defined in terms of a total function
· snapshot : PermHeap → Heap, so that ph snapshot λv ∈ Val . ph(v) snapshot , with act for some v 1 and π undefined otherwise The snapshot heap ph snapshot of any permission heap ph only contains heap cells bound by process-algebraic models, and is constructed by taking the snapshot values of all ph's action heap cells. As we shall see in a moment, the final invariant maintained by program execution safety will be ph snapshot |= pm pm, where ph and pm are taken from the models of the program logic and represent the current state of the program. This invariant, combined with establishing a refinement between the program and its abstract models, provide sufficient means for proving soundness of the program logic.

Adequacy
This section defines program execution safety and uses it to define the semantic meaning of program judgments, from which the soundness theorem (i.e., adequacy of the logic) can be formulated. Program execution safety extends on the well-known notion of configuration safety of [34], by adding permission accounting, process-algebraic state, and the machinery introduced earlier in this section.
First, in order to help connect the models of the program logic to concrete program state, we define a concretisation function for permission heaps in the same style as snapshot heaps.
Definition 34 (Concretisation). Concretisation of permission heaps is defined as a total function · concr : PermHeap → Heap, so that ph concr λv ∈ Val . ph(v) concr , with ph(v) concr defined as act for some π and v 2 undefined otherwise The heap concretisation operator constructs (program) heaps out of permission heaps by simply discarding all internal structure regarding process-algebraic models. Only the information relevant for regular program execution is retained. · snapshot essentially does the same, but only retains heap cells bound to program abstractions and takes snapshot values whenever possible.
We now have all the ingredients for defining adequacy. Program execution safety is defined in terms of a predicate safe n (C, ph, pm, s, g, R, Q), stating that C is safe for n reduction steps with respect to a permission heap ph, process map pm, two stores s and g, a resource invariant R and postcondition Q. (Program execution safety). The safe 0 (C, ph, pm, s, g, R, Q) predicate always holds, whereas safe n+1 (C, ph, pm, s, g, R, Q) holds if and only if the following five conditions hold.

2.
For every ph F and pm F such that ph ⊥ hc ph F and pm ⊥ mc pm F , it holds that ghost (C, ph ph ph F concr , pm pm pm F , s, g).

4.
For any v ∈ writes(C, s) it holds that full hc (ph(v)).

5.
For any ph J , ph F , pm J , pm F , pm C , h , s , and C such that, if: 5a. ph ⊥ ph ph J and (ph ph ph J ) ⊥ ph ph F , and 5b. pm ⊥ pm pm J and (pm pm pm J ) ⊥ pm pm F , and 5c. ¬locked(C) implies ph J , pm J , s, g |= R, and 5d.
(pm pm pm J pm pm F ) ∼ =pm pm C , and 5e. ph ph ph J ph ph F snapshot |= pm pm C , and 5f. (C, ph ph ph J ph ph F concr , s) (C , h , s ); then there exists ph , ph J , pm , pm J , pm C , and g , such that 5g. ph ⊥ ph ph J and (ph ph ph J ) ⊥ ph ph F , and 5h. pm ⊥ pm pm J and (pm pm pm J ) ⊥ pm pm F , and 5i. ph ph ph J ph ph F concr = h , and 5j.
(pm pm pm J pm pm F ) ∼ =pm pm C , and 5k. ph ph ph J ph ph F snapshot |= pm pm C , and 5l. ¬locked(C ) implies ph J , pm J , s , g |= R, and 5m.
Clarifying the above definition, any configuration is safe for n + 1 steps intuitively if: the postcondition Q is satisfied if C has terminated (1); the program C does not fault (2); C only accesses heap entries that are allocated (3); C only writes to heap locations for which full permission is available (4); and finally, after making a computation step the program remains safe for another n steps (5). (The predicate full hc (hc) is true whenever hc is an occupied heap cell with an associated fractional permission π equal to 1.) Condition 2 implies race freedom, while conditions 3 and 4 account for memory safety.
Condition 5 is particularly involved. In particular it encodes the backward simulation: if the program can do a step (5f ), then it must be able to make a matching ghost step (by 5m). Moreover, the resource invariant R must remain satisfied (due to 5c and 5l) after making a computation step, whenever the current program is not locked. In addition, the process maps invariably remain safe with respect to the snapshot heap due to 5e and 5k, as discussed in Section 3.5.3.

Lemma 10.
Program execution safety satisfies the following properties.

1.
If safe n (C, ph, pm, s, g, R, Q) and m ≤ n, then safe m (C, ph, pm, s, g, R, Q).
1 in Lemma 10 states monotonicity in the sense that being safe for n reduction steps implies safety for less than n steps. 2 in Lemma 10 states that process maps can always be replaced by bisimilar ones in safe configurations. Finally, 3 in Lemma 10 states that postconditions may always be weakened.

Semantics of Program Judgments
The semantics of program judgments is defined in terms of a quintuple Γ; R |= {P } C {Q}, expressing that C is safe for any number of reduction steps starting from any state satisfying P. (1) user(C), and (2) If |= env Γ and wf(C), then for any ph, pm, s, g such that valid ph (ph) and valid pm (pm) and ph snapshot |= pm pm and ph, pm, s, g |= P hold, it holds that: ∀n . safe n (C, ph, pm, s, g, R, Q) The underlying idea of the above definition, i.e., having a continuation-passing style definition for program judgments, has first been applied in [40] and has further been generalised in [41,42]. Moreover, the idea of defining execution safety in terms of an inductive predicate originates from [43]. These two concepts have been reconciled in [34] into a formalisation for the classical CSL of Brookes [23], that has been encoded and mechanically been proven in both Isabelle and Coq. Our definition builds on the latter, by having a refinement between programs and abstractions encoded in safe.
Observe that only judgments of user programs (i.e., commands free of runtime constructs like inatom and inact) have a semantic meaning. Also observe that the semantics of program judgments is conditional on the safety of Γ. It states that C executes safely for any number n of computation steps with respect to any state satisfying P, only if Γ is safe-that is, only if all process-algebraic models for C are (assumed to be externally) verified. From the above definition it trivially follows that Γ; R |= {false} C {P } for any Γ, R, P and user program C. Notice however that Γ; R |= {P } C {true} does not hold in general, since C might be able to fault, for example by having data-races.
The following main soundness theorem states that verified programs (i.e., programs for which a proof can be derived according to the proof rules given earlier in Figures 4 and 5) are semantically valid (that is, are fault-free, memory-safe, and refine their process-algebraic models).
The soundness proofs of all proof rules have been mechanised using the Coq proof assistant and can be found on the Git repository accompanying this article [19].
The HT-PROC-UPDATE and HT-PROC-QUERY proof rules were the most difficult to prove sound, as their proofs require, among other things, (1) showing that the abstract model can always match the program with a simulating execution step, as well as (2) maintaining the invariant that any process-algebraic abstraction inside the process map is safe with respect to the reified program state. On top of that, the combination of (1) and (2) requires some extra bookkeeping to ensure that the snapshot heaps stored in ghost metadata agree with the snapshot values stored in permission heaps. This additional bookkeeping has been left out of the formalisation presented so far, but the details of this can be studied in the Coq formalisation.

Implementation
The presented verification approach has been implemented in the VerCors concurrency verifier, which specialises in automated verification of parallel and concurrent programs written in high-level languages like (subsets of) Java and C [8]. VerCors can reason about programs with heterogeneous concurrency features as in Java, as well as homogeneous concurrency like in OpenCL, and compiler directives as in OpenMP. VerCors allows specifying (concurrent) programs with annotations from a separation logic with permission accounting. VerCors supports reasoning about freedom of data-races, memory safety and functional program behaviour-compliance of the program annotations.

Tool Support
Tool support for our technique has been implemented in VerCors for languages with fork/join concurrency and statically-scoped parallel constructs [14]. Our technique has been implemented by defining an axiomatic domain for process types in Viper, consisting of constructors for the process-algebraic connectives and standard process-algebraic axioms to support these. The three different ownership types π − → t are encoded in Viper by defining extra fields that maintain the ownership status t for each global reference. The Proc π assertions are encoded as predicates over process types.
Note however that VerCors does not yet support writing and reasoning about assertional processes ?(·). Instead, the properties to verify on a process-algebraic level are specified as postconditions of the models. That is, VerCors currently only allows reasoning about postcondition properties of process-algebraic models, while the formalisation as presented in this article allows reasoning about properties also at intermediate points of process execution. But since the formalisation is more general than the VerCors implementation (as was already indicated in Section 3.4.2), soundness is retained.
Recall that process-algebraic abstractions are to be verified externally with our approach, for example using an interactive theorem prover or a model checker like mCRL2. Nevertheless, VerCors itself also has capabilities to reason about process-algebraic models. This is done by first linearising all specified processes, and then encoding these linearised processes together with their contracts into the Viper language, and delegate further reasoning to Viper. Any process is said to be linear if it does not use the and connectives. Linearisation is a mechanical (automated) procedure based on a rewrite system that uses a subset of the bisimulation equivalences of Figure 2 as rewrite rules [44] (but in one direction only), to try to eliminate parallel connectives. For example, a process term (a 1 · a 2 ) a 3 can automatically be linearised to the bisimilar process a 1 · a 2 · a 3 + a 1 · a 3 · a 2 + a 3 · a 1 · a 2 . Note that linearisation may not always succeed. VerCors outputs a verification error in case linearlisation fails.
Process-algebraic models may also algorithmically be analysed, for example using a model checker. We are currently actively investigating the use of the mCRL2 toolset [21] and the Ivy verifier [45] to reason about (different forms of) processes, and in different use cases; see for example [17], in which we use process-algebraic abstractions to reason about distributed message passing programs.
Finally, we would like to remark that the VerCors implementation of the abstraction technique is much richer than the simple language of Section 3.2 that is used to formalise the approach on. For example, the abstraction language in VerCors supports general recursion instead of Kleene iteration. VerCors also has support for several axiomatic data types that enrich the expressivity of reasoning with program abstractions, like (multi)sets, sequences and option types.

Coq Formalisation
The formalisation and soundness proof (Sections 3.1-3.5) of the program logic have been fully mechanised using Coq, as a deep embedding inspired by [34]. The overall implementation comprises over 23.000 lines of code. The Coq development and its documentation are available online [19].

Case Study
Finally, we demonstrate our verification approach on a well-known version of the leader election protocol [46] that is based on shared memory. Most importantly, this case study shows how our approach bridges the typical abstraction gap between process algebraic models and program implementations. In particular, it shows how a high-level process algebraic model of a leader election protocol, together with a contract for this model (checked with mCRL2 for various inputs), is formally connected to an actual program implementation of the protocol, using VerCors.
The protocol is performed by N concurrent workers that are organised in a ring, so that worker i only sends to worker i + 1 and only receives from worker i − 1, modulo N. The goal is to determine a leader among these workers. To find a leader, the election procedure assumes that each worker i receives a unique integer value to start with, and then operates in N rounds. In every round, (1) each worker sends the highest value it encountered so far to its right neighbour, (2) receives a value from its left neighbour, and (3) remembers the highest of the two. The result after N rounds is that all workers know the highest unique value in the network, allowing its original owner to announce itself as leader.
The case study has been verified with VerCors using the presented approach. All workers communicate via two standard non-blocking operations for message passing: mp_send(r,msg) for sending a message msg to the worker with rank r, and msg := mp_recv(r) for receiving a message from worker r. (The identifiers of workers are typically called ranks in message passing terminology. Ranks are simply natural numbers.) The election protocol is implemented on top of this message passing system.
The main challenge of this case study is to define a message passing system on the process algebra level that matches this implementation. To design such a system we follow the ideas of [46]; by defining two actions, send(r, msg) and recv(r, msg), that abstractly describe the behaviour of the concrete implementations in mp_send and mp_recv, respectively. Process algebraic summation Σ x P is used to quantify over the possible messages that mp_recv might receive.
The following two rules illustrate how the abstract send and recv actions are connected to the mp_send and mp_recv procedures in the program, respectively. The latter rule uses a summation (shorthand) of the form Σ x∈Msg P that can be considered equivalent to the process Σ x (x ∈ Msg : P).

Case Study
Finally, we demonstrate our verification approach on a well-known version of the leader election protocol [46] that is based on shared memory. Most importantly, this case study shows how our approach bridges the typical abstraction gap between process algebraic models and program implementations. In particular, it shows how a high-level process algebraic model of a leader election protocol, together with a contract for this model (checked with mCRL2 for various inputs), is formally connected to an actual program implementation of the protocol, using VerCors.
The protocol is performed by N concurrent workers that are organised in a ring, so that worker i only sends to worker i + 1 and only receives from worker i − 1, modulo N. The goal is to determine a leader among these workers. To find a leader, the election procedure assumes that each worker i receives a unique integer value to start with, and then operates in N rounds. In every round, (1) each worker sends the highest value it encountered so far to its right neighbour, (2) receives a value from its left neighbour, and (3) remembers the highest of the two. The result after N rounds is that all workers know the highest unique value in the network, allowing its original owner to announce itself as leader.
The case study has been verified with VerCors using the presented approach. All workers communicate via two standard non-blocking operations for message passing: mp_send(r,msg) for sending a message msg to the worker with rank r, and msg := mp_recv(r) for receiving a message from worker r. (The identifiers of workers are typically called ranks in message passing terminology. Ranks are simply natural numbers.) The election protocol is implemented on top of this message passing system.
The main challenge of this case study is to define a message passing system on the process algebra level that matches this implementation. To design such a system we follow the ideas of [46]; by defining two actions, send(r, msg) and recv(r, msg), that abstractly describe the behaviour of the concrete implementations in mp_send and mp_recv, respectively. Process algebraic summation Σ x P is used to quantify over the possible messages that mp_recv might receive.
The following two rules illustrate how the abstract send and recv actions are connected to the mp_send and mp_recv procedures in the program, respectively. The latter rule uses a summation (shorthand) of the form Σ x∈Msg P that can be considered equivalent to the process Σ x (x ∈ Msg : P).
{send(r, msg) · P} mp_send(r,msg) { P} {Σ x∈Msg recv(r, x) · P} msg := mp_recv(r) { P[x/msg]} Finally, we construct a process-algebraic model of the election protocol using send and recv, and verify that the implementation adheres to this model. This model has been analysed with mCRL2 for various inputs (since mCRL2 is essentially finite-state) to establish the global property of announcing the correct leader. The deductive proof of the program can then rely on this property.

Behavioural Specification
The main goal is proving that the implementation determines the correct leader upon termination. To prove this we first define a behavioural specification of the election protocol that hides all irrelevant implementation details, and prove the correctness property on this specification. Process algebra provides a proper abstraction language that suits our needs well, as the behaviour of leader election can concisely be specified in terms of sequences of sends and receives. Figure 8 presents the process algebraic specification. In particular, ParElect specifies the global behaviour of the program whereas Elect specifies its thread-local behaviour. The ParElect process encodes the parallel composition of all eligible participants. ParElect takes a sequence vs of initial values as argument, whose length equals the total number of workers by its precondition. ParElect's postcondition (i.e., trailing assertion) states that lead must be a valid rank after termination and that vs[lead] be the highest initial worker value. It follows that worker lead is the correctly chosen leader.
Finally, we construct a process-algebraic model of the election protocol using send and recv, and verify that the implementation adheres to this model. This model has been analysed with mCRL2 for various inputs (since mCRL2 is essentially finite-state) to establish the global property of announcing the correct leader. The deductive proof of the program can then rely on this property.

Behavioural Specification
The main goal is proving that the implementation determines the correct leader upon termination. To prove this we first define a behavioural specification of the election protocol that hides all irrelevant implementation details, and prove the correctness property on this specification. Process algebra provides a proper abstraction language that suits our needs well, as the behaviour of leader election can concisely be specified in terms of sequences of sends and receives. Figure 8 presents the process algebraic specification. In particular, ParElect specifies the global behaviour of the program whereas Elect specifies its thread-local behaviour. The ParElect process encodes the parallel composition of all eligible participants. ParElect takes a sequence vs of initial values as argument, whose length equals the total number of workers by its precondition. ParElect's postcondition (i.e., trailing assertion) states that lead must be a valid rank after termination and that vs[lead] be the highest initial worker value. It follows that worker lead is the correctly chosen leader. 21 / * The local behavioural specification for every worker. * / 22 requires 0 ≤ n ≤ |chan| ∧ 0 ≤ rank < |chan|; 23 process Elect(int rank, The Elect process takes four arguments, which are: the rank of the worker, the initial unique value v 0 of that worker, the current highest value v encountered by that worker, and finally the number n of remaining rounds. The rounds are implemented via general recursion. In each round all workers send their current highest value v to their right neighbour (on line 24), receive a value v from their left neighbour (line 25), and continue with the highest of the two. The announce action is declared and used to announce the leader after n rounds. The effect of announce is that lead stores the leader's rank.
The contracts of send and recv describe the behaviour of standard non-blocking message passing. Communication on the specification level is implemented via message queues. Message queues are defined as sequences of messages taken from a domain Msg. Since workers are organised in a ring it suffices to have only a single queue for every worker, meaning that the global communication channel architecture can be defined as a sequence of message queues: chan in the figure. The action contract of send(r, msg) expresses enqueuing msg onto the message queue chan[r] of the worker with rank r. The effect of send is that msg has been enqueued onto chan [r] and that the queues chan[r ] for any r = r have not been altered. Likewise, recv(r, msg)'s contract expresses dequeuing msg from chan[r]. The expression \old(e) indicates that e is to be evaluated with respect to the pre-state of computation. The Elect process takes four arguments, which are: the rank of the worker, the initial unique value v 0 of that worker, the current highest value v encountered by that worker, and finally the number n of remaining rounds. The rounds are implemented via general recursion. In each round all workers send their current highest value v to their right neighbour (on line 24), receive a value v from their left neighbour (line 25), and continue with the highest of the two. The announce action is declared and used to announce the leader after n rounds. The effect of announce is that lead stores the leader's rank.
The contracts of send and recv describe the behaviour of standard non-blocking message passing. Communication on the specification level is implemented via message queues. Message queues are defined as sequences of messages taken from a domain Msg. Since workers are organised in a ring it suffices to have only a single queue for every worker, meaning that the global communication channel architecture can be defined as a sequence of message queues: chan in the figure. The action contract of send(r, msg) expresses enqueuing msg onto the message queue chan[r] of the worker with rank r. The effect of send is that msg has been enqueued onto chan [r] and that the queues chan[r ] for any r = r have not been altered. Likewise, recv(r, msg)'s contract expresses dequeuing msg from chan[r]. The expression \old(e) indicates that e is to be evaluated with respect to the pre-state of computation. Figure 9 presents the annotated implementation of the election protocol. (It should be noted here that the presentation is slightly different from the version that is verified with VerCors, to better connect to the theory discussed in the earlier sections to the case study. This is because VerCors uses Implicit Dynamic Frames [47] as its underlying logical framework, which is equivalent to separation logic [48] but handles ownership slightly differently. The details of this are deferred to [8,49].) The elect method contains the code that is executed by every worker. The contract of elect(rank, v 0 , v) states that the method body adheres to the behavioural specification Elect(rank, v 0 , v, N) of the election protocol. Each worker performing elect enters a for-loop that iterates N times, whose loop invariant states that, at iteration i, the remaining program behaves as prescribed by the process Elect(rank, v 0 , v, i − 1). The invocations to mp_send and mp_recv on lines 32 and 36 are annotated with with clauses that resolve the assignments required by the given clauses in the contracts of mp_send and mp_recv. The given η annotation expresses that the parameter list η are extra ghost arguments for the sake of specification. Stated differently, η is a sequence of logical variables which are universally quantified at the (outer) level of the method contract (the types of these are left implicit for ease of presentation). After N rounds all workers with v = v 0 announce themselves as leader. However, since the initial values are chosen to be unique there can only be one such worker. Finally, we can verify that at the post-state of elect the abstract model has been fully executed and thus reduced to ε in the logic.

Protocol Implementation
The mp_send(rank, msg) method implements the operation of enqueuing msg onto the message queue of worker rank. Its implementation has been omitted for brevity. The contract of mp_send expresses that the enqueuing operation is abstracted as a send(rank, msg) action that is prescribed by an abstract model identified by X. The mp_recv(X, rank) method implements the operation of dequeuing and returns the first message of the message queue of worker rank. The receive is prescribed as an abstract recv action, where the received message is ranged over by the summation on line 17. Figure 10 presents bootstrapping code for the implementation of message passing. The main procedure initialises the communication channels whereas parelect spawns all workers. main(vs) additionally initialises and finalises the abstraction ParElect(vs) on the specification level (on lines 76 and 83, resp.) whose analysis allows establishing main's postconditions. The procedure parelect(vs) implements the abstract model ParElect(vs) by spawning N workers that all execute the elect program. The contract associated to the parallel block (lines 55-59) is called an iteration contract and assigns preand postconditions to every parallel instance. For more details on iteration contracts we refer to [50]. Most importantly, the iteration contract of each parallel worker states (on line 58) that it behaves as specified by Elect. Thus, we deductively verify in a thread-modular way that the program implements its behavioural specification. Observe that all the required ownership for the global fields and the Proc 1 predicate is split and distributed among the individual workers via the iteration contract and the with clause on lines 62-64. Finally, the main correctness property is conveyed from the process level to the program level by the query X annotation on line 82, which "queries" for ParElect's postconditions. given X, P, Q, Π, π, π ; 8 context {chan → C} ∈ Π; 9 context ∃n . N π − → proc n * 0 ≤ rank < n; 10 requires Proc π (X, send(rank, msg) · P + Q, Π); 11 ensures Proc π (X, P, Π); 12 void mp_send(int rank, Msg msg) { / * omitted * / } 13 14 given X, P, Q, Π, π, π ; 15 context {chan → C} ∈ Π; 16 context ∃n . N π − → proc n * 0 ≤ rank < n; 17 requires Proc π (X, (Σ x∈Msg recv(rank, x) · P) + Q, Π); 18 ensures Proc π (X, P[x/\result], Π); 19 Msg mp_recv(int rank) { / * omitted * / } 20 21 given X, n, Π, π, π ; 22 context {lead → L, chan → C} ∈ Π; 23 context N π − → proc n * 0 ≤ rank < n; 24 requires Proc π (X, Elect(rank, v 0 , v, n), Π); 25 ensures Proc π (X, ε, Π); 26

Specification and Verification Details
The VerCors encoding of the presented leader election protocol comprises 433 lines of code, of which 275 are specification annotations (63, 5% of the total) and 27 lines are comments (6, 2% of the total). Out of the 275 lines of specification code, 62 are used for specifying the process-algebraic model (22, 5%), and the remaining 213 lines (77, 5%) for formally linking this model to the program code.
The average verification time with VerCors is 19, 75s (average of 30 runs), measured on a Macbook with an Intel Core i5 CPU with 2,9 GHz and 8Gb memory. All verification files are available online [19].

Industrial Applicability
Apart from the presented leader election case study, our approach has been applied in a larger, industrial case study covering the formal verification of a traffic tunnel emergency control system [15]. In this case study, we successfully verified a safety-critical component of an emergency control system

Specification and Verification Details
The VerCors encoding of the presented leader election protocol comprises 433 lines of code, of which 275 are specification annotations (63, 5% of the total) and 27 lines are comments (6, 2% of the total). Out of the 275 lines of specification code, 62 are used for specifying the process-algebraic model (22, 5%), and the remaining 213 lines (77, 5%) for formally linking this model to the program code.
The average verification time with VerCors is 19, 75s (average of 30 runs), measured on a Macbook with an Intel Core i5 CPU with 2,9 GHz and 8Gb memory. All verification files are available online [19].

Industrial Applicability
Apart from the presented leader election case study, our approach has been applied in a larger, industrial case study covering the formal verification of a traffic tunnel emergency control system [15]. In this case study, we successfully verified a safety-critical component of an emergency control system of an actual traffic tunnel that is currently in use in the Netherlands. This particular software component is responsible for handling any emergency situations that occur inside the traffic tunnel. For example, whenever a fire breaks out inside the tunnel or an accident occurs, it must start an emergency procedure to evacuate all people and turn on the emergency lights to help guide them out; control the fans to blow away any smoke; et cetera. Naturally the reliability demands imposed on such a software component are very high. Our research goal was to see if formal verification could help.
Our approach for this case study was to use mCRL2 to construct a formal, process-algebraic model of the software design, which was written informally as a state machine together with pseudo-code descriptions of the different system behaviours. We then analysed the state space of this model and checked whether it satisfies desirable properties, which we composed together with the company that wrote the actual code. Ultimately we found problematic behaviour: the system could, due to an unlucky combination of timing and events, reach a calamity state in which the emergency procedure is not started. However, the software company already knew of this problematic behaviour and deliberately provided us with an older version of their software. Nevertheless, we demonstrated that formal methods can indeed help to improve the quality of real-world industrial software.
In addition to modelling and analysing the software design with mCRL2, we also used VerCors to prove that the actual code implementation is soundly abstracted by the process-algebraic model, using the techniques presented in this article. We did this to increase the value of our formal model, and to ensure that its analysis is meaningful. Moreover, our verification also (indirectly) proved that the code implementation adheres to the pseudo-code specifications in the original software design.
Overall this case study highlights how the presented approach is applicable to real-world projects. We were able to identify potential vulnerabilities in the software design, and could link (a formal model of) the software design to the actual code. We are currently involved in a follow-up project with the same company, and aim to apply our technique during the software development process.

Related Work
Significant progress has been made on the theory of concurrent software verification over the last years [1-5, [51][52][53]. This line of research proposes advanced program logics that all provide some notion of expressing and restricting thread interference of various complexity, via protocols [54]-formal descriptions of how shared program state is allowed to evolve over time. In our approach protocols have the form of processes.
The original work on CSL [24] allows specifying simple thread interference in shared-memory concurrent programs via resource invariants and critical regions. Later, RGSep [55] merges CSL with rely-guarantee reasoning to enable describing more fine-grained inter-thread interference by identifying atomic concurrent actions. Many modern program logics build on these principles and propose even more advanced ways of verifying shared-memory concurrency. For example, TaDa [5] and CaReSL [3] express thread interference protocols through state-transition systems. iCAP [51] and Iris [56] propose a more unified approach by accepting user-defined monoids to express protocols on shared state, together with invariants restricting these protocols. Iris provides reasoning support for proving language properties in Coq, whereas our focus is on proving (concrete) programs correct.
In the distributed setting, Disel [6] allows specifying protocols for distributed systems. Disel builds on dependent type theory and is implemented as a shallow embedding in Coq. Even though their approach is more expressive than ours, it has to be used in the context of Coq and thus can be applied only semi-automatically at the moment. Villard et al. [57] present a program logic for message passing concurrency, where threads may communicate over channels using native send/receive primitives. This program logic allows specifying protocols via contracts, which are state-machines in the style of Session Types [58] to describe channel behaviour. Our technique is more general however, as the approach of Villard et al. is tailored specifically to reason about basic shared-memory message passing (Section 5 for example demonstrates how a system of message passing can be realised using process-algebraic abstractions). Actor Services [59] is a program logic with assertions to express the consequences of asynchronous message transfers between actors-independent program units that communicate via message passing. The meta-theory of Actor Services has not been proven sound.
Most of the related work given so far is essentially theoretical and tend to focus primarily on expressivity-on contributing approaches that are expressive yet not necessarily easy to implement into SMT-based program verifiers like for example VerCors of Viper. In fact, for most of these approaches it is very challenging to implement such automated tool support. Instead, they have to be applied in pen-and-paper style, or in the context of an interactive theorem prover like Coq or Isabelle. Our abstraction approach is different, in the sense that its aim is not to maximise expressivity (for example by integrating into higher-order separation logics, like for example [60]). Instead, we aim for a verification approach that balances expressivity and usability-an approach that is expressive enough to reason about real-world concurrent programs that follow some protocol, while being implementable into automated code verifiers; in this case, VerCors and Viper.
Related concurrency verifiers are SmallfootRG [61], VeriFast [7], CIVL [62], THREADER [63] and Viper [9,10]; the latter tool is used as the main back-end of VerCors. SmallfootRG is a memory-safety verifier based on RGSep. VeriFast is a rich toolset for verifying (multi-threaded) Java and C programs using separation logic. The CIVL framework can reason about race-freedom and functional correctness of MPI programs written in C [64,65]. The reasoning is done via bounded model checking combined with symbolic execution. THREADER is an automated verifier for multi-threaded C, based on model checking and counterexample-guided abstraction refinement.
One approach that is particularly noteworthy is the one of Penninckx et al. [66], who propose a logic to specify and verify input/output (I/O) behaviour of (sequential) programs. The logic has been implemented in VeriFast. In this approach the I/O behaviour of programs is specified essentially as a Petri Net. Its assertion language has constructs to specify I/O permission tokens, and its proof system has inference rules that allow reducing a Petri Net specification alongside the structure of the program, similar to our approach. However, their specification/verification strategy is to make predictions about the behaviour of the environment (which may or may not turn out true), by specifying assumptions on what the environment will input given a particular I/O operation and output, which is in contrast to our approach. In fact, with our approach one could analyse and use process-algebraic models together with an extra process that models an environment, to achieve stronger reasoning capabilities.
Apart from the proposed technique, VerCors also allows using process algebraic abstractions as histories [67,68]. Also related in this respect are the time-stamped histories of [69], which records atomic state changes in concurrent programs as a history, which are, likewise to our approach, handled as resources in the logic. However, history recording is only suitable for terminating programs.
There is also related earlier work on using process-algebraic abstractions to reason about message passing distributed programs [17]. This work introduces the assertional processes ?(·) as well as process-algebraic summation, as they are used in this article. In fact, this article merges the core ideas of [16,17] into a single logical framework, to make the original work of [16] more general.
Finally, there is a lot of general work on proving linearisability [70][71][72], which essentially allows reasoning about fine-grained concurrency by using sequential verification techniques. Our technique, as well as the history-based technique of [67] uses process algebraic linearisation to do so.

Conclusions
To reason effectively about realistic concurrent and distributed software, we have presented a verification technique that performs the reasoning at a suitable level of abstraction that hides irrelevant implementation details, is scalable to realistic programs by being modular and compositional, and is practical by being supported by automated tools. The approach is expressive enough to allow reasoning about realistic software as is demonstrated by the case study as well as by [15], and can be implemented as part of an automated deductive SMT-based program verifier, viz. VerCors. The proof system underlying our technique has mechanically been proven sound using Coq. Our technique is therefore supported by a strong combination of theoretical justification and practical usability.