Automating Deductive Verification for Weak-Memory Programs

Writing correct programs for weak memory models such as the C11 memory model is challenging because of the weak consistency guarantees these models provide. The first program logics for the verification of such programs have recently been proposed, but their usage has been limited thus far to manual proofs. Automating proofs in these logics via first-order solvers is non-trivial, due to reasoning features such as higher-order assertions, modalities and rich permission resources. In this paper, we provide the first implementation of a weak memory program logic using existing deductive verification tools. We tackle three recent program logics: Relaxed Separation Logic and two forms of Fenced Separation Logic, and show how these can be encoded using the Viper verification infrastructure. Many ingredients of our encoding are not specific to the particular logics tackled, and we explain the general techniques applied. We evaluate the encoding on examples from existing papers and on code from an open-source library.


Introduction
Reasoning about programs running on weak memory is challenging because weak memory models admit executions that are not sequentially consistent, that is, cannot be explained by a sequential interleaving of concurrent threads. Moreover, weak-memory programs employ a range of operations to access memory, which require dedicated reasoning techniques. These operations include fences as well as read and write accesses with varying degrees of synchronisation.
Some of these challenges are addressed by the first program logics for weakmemory programs, in particular, Relaxed Separation Logic (RSL) [28], GPS [26], Fenced Separation Logic (FSL) [13], and FSL++ [12]. These logics apply to interesting classes of C11 programs, but their tool support has been limited to embeddings in Coq. Verification based on these embeddings requires substantial user interaction, which is an obstacle to applying and evaluating these logics.
In this paper, we present a novel approach to automating deductive verification for weak memory programs. We encode large fractions of RSL, FSL, and FSL++ into the intermediate verification language Viper [19], and use the existing Viper verification backends to reason automatically about the encoded programs. This encoding reduces all concurrency and weak-memory features as well as logical

Non-atomic Locations
We present our encoding for a small imperative programming language that reflects the core operations relevant for C-like programs using C11 atomics, and is similar to the languages supported by the RSL logics. C11 supports non-atomic memory accesses and different forms of atomic accesses. The access operations are summarised in Fig. 1. We adopt the common simplifying assumption [28,26] that memory locations are partitioned into those accessed only via non-atomic accesses (non-atomic locations), and those accessed only via C11 atomics (atomic locations). Read and write statements are parameterised by a mode σ, which is either na (non-atomic) or one of the atomic access modes 1 τ . We focus on non-atomic accesses in this section and discuss atomics in subsequent sections.
RSL proof rules. Non-atomic memory accesses come with no synchronisation guarantees; programmers are obliged to ensure that all accesses to non-atomic locations are data-race free. The RSL logics enforce this requirement using standard separation logic [20,23]. We show the syntax of assertions in Fig. 2, which will be explained throughout the paper. A points-to assertion l k → e denotes a transferrable resource, providing permission to access the location l, and expressing that the location has been initialised and its current value is the Write access requires either write permission or that the location is uninitialised. The underscore _ stands for an arbitrary value.
value of expression e. Here, k is a fraction 0 < k ≤ 1; k = 1 denotes the full (or exclusive) permission to read and write location l, whereas 0 < k < 1 provides (non-exclusive) read access [7]. Points-to resources can be split and recombined, but never duplicated or forged; when transferring such a resource to another thread it is removed from the current one, avoiding data races by construction. The RSL assertion Uninit(l) expresses exclusive access to a location l that has been allocated, but not yet initialised; l may be written to but not read from (in C11 this would be undefined behaviour). The main proof rules for non-atomic locations, adapted from RSL [28], are shown in Fig. 3.
Encoding. The Viper intermediate verification language [19], which we use to encode the RSL logics, supports an assertion language based on Implicit Dynamic Frames [25], a program logic related to separation logic [21], but which separates permissions from value information. Viper is object-based; the only memory locations are field locations e.f (in which e is a reference, and f a field name). Permissions to access these heap locations are described by accessibility predicates of the form acc(e.f , k), where k is a fraction as for points-to predicates above (k defaults to 1). Assertions that do not contain accessibility predicates are called pure. Unlike in separation logics, heap locations may be read in pure assertions. We model C-like memory locations l using a field val of a Viper reference l. Consequently, a separation logic assertion l k → e is represented in Viper as acc(l.val, k) && l.val == e. In this paper, we assume that memory locations have type int, but a generalisation is trivial. Viper's conjunction && treats permissions multiplicatively, requiring the sum of the permissions in each conjunct, and acts as logical conjunction for pure assertions (just as * in separation logic).
Viper provides two key statements for encoding proof rules. An inhale statement inhale A has the effect of adding the permissions denoted by the Viper assertion A to the current state, and assuming pure assertions in A. This can be used to model gaining new resources, e.g., acquiring a lock in the source program. Dually, exhale A checks that the current state satisfies A (otherwise a verification error occurs), and removes the permissions that A denotes; the values of any locations to which no permission remains are havoced (assigned arbitrary values). For example, when forking a new thread, its precondition is exhaled to reflect transferring ownership of the necessary resources from the forking thread. Inhale  and exhale statements can be seen as the permission-aware analogues of the assume and assert statements of first-order verification languages [17].
The encoding of the rules for non-atomics from Fig. 3 is presented in Fig. 4 . . . for source-level statements s. The first two lines show background declarations employed in our encoding. The assertion encodings follow the explanations above. Allocation is modelled by obtaining a fresh reference (via new()) and inhaling permissions to its val and init fields; Assuming !l.init reflects that the location is not yet initialised. Viper implicitly checks the necessary permissions for field accesses (verification fails otherwise). Hence, the translation of a non-atomic reads only needs to check that the read location is initialised before obtaining its value. Analogously, because of Viper's implicit permission check, the translation of a non-atomic write only stores the value and records that the location is now initialised.
Note that Viper's implicit permission checks are both necessary and sufficient to encode the RSL rules in Fig. 3. In particular, the assertions l 1 → _ and Uninit(l) both provide the permissions to write to location l. By including acc(l.val) in the encoding of both assertions, we avoid the disjunction of the RSL rule 2 .
Our approach requires programmers to annotate their code with access modes for locations (as part of the alloc statement), and specifications such as pre and postconditions for methods and threads. Given these inputs, Viper constructs the proof automatically. In particular, it automatically proves entailments, and splits and combines fractional permissions (hence, the equivalence in Fig. 3 need not be encoded). Automation can be increased further by inferring some of the required assertions, but this is orthogonal to the encoding presented in this paper.

Release-Acquire Atomics
The simplest form of C11 atomic memory accesses are release write and acquire read operations. They can be used to synchronise the transfer of ownership of (and information about) other, non-atomic locations, using a message passing idiom, illustrated by the example in Fig. 5. This program allocates two non-atomic locations a and b, and an atomic location l (initialised to 0), which is used to synchronise the three threads that are spawned afterwards. The middle thread makes changes to the non-atomics a and b, and then signals completion via a release write of 1 to l; conceptually, it gives up ownership of the non-atomic locations via this signal. The other threads loop attempting to acquire read a non-zero value from l. Once they do, they each gain ownership of one non-atomic location via the acquire read of 1 and access that location. The release write and acquire reads of value 1 enforce ordering constraints on the non-atomic accesses, preventing the left and right threads from racing with the middle one.
RSL proof rules. The RSL logics capture message-passing idioms by associating a location invariant Q with each atomic location. Such an invariant is a function from values to assertions; we represent such functions as assertions with a distinguished variable symbol V as parameter. Location invariants prescribe the intended ownership that a thread obtains when performing an acquire read of value V from the location, and that must correspondingly be given up by a thread performing a release write. The main proof rules [28] are shown in Fig. 6.
When allocating an atomic location for release/acquire accesses (first proof rule), a location invariant Q must be chosen (as an annotation on the allocation). The assertions Rel(l, Q) and Acq(l, Q) record the invariant to be used with subsequent release writes and acquire reads. To perform a release write of value e (second rule), a thread must hold the Rel(l, Q) assertion and give up the assertion Q[e/V]. For example, the line [l] rel := 1 in Fig. 5 causes the middle thread to give up ownership of both non-atomic locations a and b. The assertion Init(l) represents that atomic location l is initialised; both Init(l) and Rel(l, Q) are duplicable assertions; once obtained, they can be passed to multiple threads.
Multiple acquire reads might read the value written by a single release write operation; RSL prevents ownership of the transferred resources from being obtained (unsoundly) by multiple readers in two ways. First, Acq(l, Q) assertions cannot be duplicated, only split by partitioning the invariant Q into disjoint parts. For example, in Fig. 5, Acq(l, Q 1 ) is given to the left thread, and Acq(l, Q 2 ) to the right. Second, the rule for acquire reads adjusts the invariant in the Acq assertion such that subsequent reads of the same value will not obtain any ownership.

Encoding.
A key challenge for encoding the above proof rules is that Rel and Acq are parameterised by the invariant Q; higher-order assertions are not directly supported in Viper. However, for a given program, only finitely many such parameterisations will be required, which allows us to apply defunctionalisation [22], as follows. Given an annotated program, we assign a unique index to each syntactically-occurring invariant Q (in particular, in allocation statements, and as parameters to Rel and Acq assertions in specifications). Furthermore, we assign unique indices to all immediate conjuncts of these invariants. For the program in Fig. 5, we assign indices 1, 2 and 3 to invariants Q 1 , Q 2 and Q 1 * Q 2 , respectively. We write indices for the set of indices used. For each i in indices, we write inv(i) for the invariant which i indexes. For an invariant Q from the source program, we write Q for its index, and Q for the set of indices assigned to its immediate conjuncts. In our example, Our encoding of the RSL rules from Fig. 6 is summarised in Fig. 7. To encode duplicable assertions such as Init(l), we make use of Viper's wildcard permissions [19], which represent unknown positive permission amounts. When exhaled, these amounts are angelically chosen [14]; provided some positive permission is held, the amount exhaled will always be strictly smaller. So after inhaling an Init(l) assertion (that is, a wildcard permission), it is possible to exhale two wildcard permissions, corresponding to two Init(l) assertions. Note that for atomic locations, we only use the init field's permissions, not its value.
We represent a Rel(l, _) assertion for some invariant via a wildcard permission to a rel field; this is represented via the SomeRel(l) macro 3 , and is used as the precondition for a release write (we must hold some Rel assertion, according to Fig. 6). The specific invariant associated with the location l is represented by 3 Viper macros can be defined for assertions or statements, and are syntactically expanded (and their arguments substituted) on use.  storing its index as the value of the rel field; when encoding a release write, we branch on this value to exhale the appropriate assertion 4 .
Analogously to Rel, we represent an Acq assertion for some invariant using a wildcard permission (the SomeAcq macro), which is the precondition for executing an acquire read. However, to support splitting, we represent the invariant in a more fine-grained way, by recording individual conjuncts separately. Each conjunct i of the invariant is modelled as an abstract predicate instance AcqConjunct(l, i), which can be inhaled and exhaled individually, just like individual field permissions. This encoding handles the common case that invariants are split along top-level conjuncts, as in Fig. 5. More complex splits can be supported through additional annotations, see App. C.
A release write is encoded by checking that some Rel assertion is held, and then exhaling the associated invariant for the value written. Moreover, it records that the location is initialised. The RSL rule for acquire reads adjusts the Acq invariant by obliterating the assertion for the value read. Instead of directly representing the adjusted invariant (which would complicate our numbering scheme), we track the set of values read as state in our encoding. We complement each AcqConjunct predicate instance with an (uninterpreted) Viper function valsRead(l, i) depending on it, returning a set of indices 5 .
A require read checks that the location is initialised and that we have some Acq assertion for the location. It assigns an unknown value to the lhs variable x, which is subsequently constrained by the invariant associated with the Acq assertion as follows: We check for each index whether we both currently hold an AcqConjunct predicate for that index 6 , and if so, have not previously read the value x from that conjunct of our invariant. If these checks succeed, we inhale the indexed invariant for x, and then include x in the values read.
The encoding presented so far allows us to automatically verify annotated C11 programs using release writes and acquire reads (e.g., the program of Fig. 5) without any custom proof strategies [3]. In particular, we can support the higherorder Acq and Rel assertions through defunctionalisation and enable the splitting of invariants through a suitable representation.

Relaxed Memory Accesses and Fences
In contrast to release-acquire accesses, C11's relaxed atomic accesses provide no synchronisation. Consequently, RSL's proof rules for relaxed atomics provide weak guarantees, in particular, do not support ownership transfer.
Memory fence instructions can eliminate this problem. Intuitively, a release fence together with a subsequent relaxed write allows a thread to transfer away ownership of resources, similar to a release write (provided that the transferred resources are not accessed between the fence and the relaxed write). Dually, an acquire fence together with a prior relaxed read allows a thread to obtain ownership of resources, similar to an acquire read (again, provided that the transferred resources are not accessed between the relaxed read and the fence); this reasoning is justified by the ordering guarantees of the C11 model [13].
FSL proof rules. FSL and FSL++ provide proof rules for fences (see Fig. 8). Theyuse modalities ("up") and ("down") to represent resources that are transferred through relaxed accesses and fences. An assertion A represents a resource A which has been prepared, via a release fence, to be transferred by a relaxed write operation; dually, A represents resources A obtained via a relaxed read, which may not be made use of until an acquire fence is encountered. The and analogously for and other binary connectives proof rule for relaxed write is identical to that for a release write (cf. Fig. 6), except that the assertion to be transferred away must be under the modality; this can be achieved by the rule for release fences. The rule for a relaxed read is the same as that for acquire reads, except that the gained assertion is under the modality. The modality can be removed by a subsequent acquire fence. Finally, assertions may be rewritten under modalities, and both modalities distribute over all other logical connectives.
Encoding. The main challenge in encoding the FSL rules for fences is how to represent the two new modalities. Since these modalities guard assertions which cannot be currently used or combined with modality-free assertions, we model them using two additional heaps to represent the assertions under each modality. The program heap (along with associated permissions) is a built-in notion in Viper, and so we cannot directly employ three heaps. Therefore, we construct the additional "up" and "down" heaps, by axiomatising bijective mappings up and down between a real program reference and its counterparts in these heaps. That is, technically our encoding represents each source location through three references in Viper's heap (rather than one reference in three heaps). Assertions A are then represented by replacing all references r in the encoded assertion A with their counterpart up(r). We write A up for the transformation which performs this replacement. We write A down for the analogous transformation for the down function.
The extension of our encoding is shown in Fig. 9. We employ a Viper domain to introduce and axiomatise the mathematical functions used to represent our up and down mappings. By axiomatising inverses for these mappings, we guarantee bijectivity. Bijectivity allows Viper to conclude that (dis)equalities and other information is preserved under these mappings. Consequently, we do not have to explicitly encode the last two rules of Fig. 8; they are reduced to standard assertion manipulations in our encoding.
An additional heap function labels references with an integer identifying the heap to which they belong (we use 0 for real references, and "down" and "up" counterparts, respectively); this labelling provides the verifiers with the (important) information that these notional heaps are disjoint.
Our handling of relaxed read and write is almost identical to that of acquire reads and release writes in Fig. 7; this similarity comes from the original proof rules, which only require that the modalities be inserted when dealing with the invariant. Our encoding for release fences requires an annotation in the source program to indicate which assertion to prepare for release by placing it under the modality. Inferring these assertions automatically is future work. For the dual case of acquire fences, we can provide an encoding which does not require any user annotations. Any assertion that is under the modality can (and should) be converted to its corresponding version without the modality because A is strictly less-useful than A itself. We encode this conversion as follows. We find all permissions currently held in the down heap, and transfer the permissions to (and values of) these locations over to the real heap. We encode this conversion for each field and predicate separately; the steps for the val field are shown at the end of Fig. 9. We first define a set rs to be precisely the set of all references r to which some permission to the field location down(r).val is currently held, i.e., perm(down(r).val) > none. Then, for each such location we inhale exactly the same amount of permission to the corresponding r.val location. Viper allows such pointwise updates to the permissions held, via its support for permissions under quantifiers [19,18]. Having added permission to these locations, we can equate the heap values, and then remove the permission to the down locations, completing the conversion of these locations. With our encoding based on multiple heaps, reasoning about assertions under modalities automatically inherits all of the natively-supported automation that Viper provides for permission and heap reasoning. We will reuse this idea for a different purpose in the following section.

Compare and Swap
C11 includes atomic read-modify-write operations, commonly used to implement high-level synchronisation primitives such as locks. In particular, the FSL++ logic [12] provides proof rules for compare and swap (CAS) operations, as supported natively by many architectures and libraries. An atomic compare and swap CAS τ (l, e, e ) reads and returns the value of location l; if the value read is equal to e, it also writes the value e (otherwise we say that the CAS fails). FSL++ proof rules. The latest proof rules for C11 CAS operations come from FSL++. They extend the previously-presented logic with a new assertion RMWAcq(l, Q), which is similar to Acq(l, Q), but is used for transferring ownership via CAS operations instead of acquire reads. The essential idea is for a successful CAS operation to both obtain ownership of an assertion via its read operation, and give up ownership of an assertion via its write operation. FSL++ does not support general combinations of atomic reads and CAS operations on the same location; the way of reading must be chosen at allocation via the annotation ρ on the allocation statement (see Fig. 1). In contrast to the Acq assertions used for atomic reads, RMWAcq assertions can be freely duplicated and their invariants need not be rewritten for a successful CAS: when using only CAS operations, each value read from a location corresponds to a different write.  Our presentation of the relevant proof rules is shown in Fig. 10. Allocating a location with annotation RMW provides a Rel and a RMWAcq assertion, such that the location can be used for release writes and CAS operations.
For the CAS operation, we present a single, general proof rule instead of four rules for the different combinations of access modes in FSL++ 7 . The rule requires that l is initialised (since its value is read), Rel and RMWAcq assertions (the fact that Rel is required is a technicality, which we will not explain here), and an assertion P that provides the resources needed for a successful CAS. If the CAS fails (that is, x = e), its precondition is preserved.
If the CAS succeeds, it has read value e and written value e . Assuming for now that the access mode τ permits ownership transfer, the thread has acquired Q[e/V] and released Q[e /V]. As illustrated in Fig. 11(i), these assertions may overlap. Let T denote the assertion characterizing the overlap; then assertions A denotes Q[e/V] without the overlap, and P denotes Q[e /V] without the overlap. The net effect of a successful CAS is then to acquire A and to release P , whereas T remains with the location invariant across the CAS. The proof rule is phrased in terms of T , A, and P . Automating the choice of these parameters is one of the main challenges of encoding this rule. Finally, if the access mode τ does not permit ownership transfer (that is, fences are needed to perform the transfer), A and P are put under the appropriate modalities.
Encoding. Our encoding of CAS operations uses several techniques presented in earlier sections; see App. E for details. We represent RMWAcq assertions analogously to our encoding of Acq assertions (see Sec. 3). We use the value of field acq (cf. Fig. 7) to differentiate between holding some RMWAcq assertion from some Acq assertion. Since RMWAcq assertions are duplicable (cf. Fig. 10), we employ wildcard permissions for the corresponding AcqConjunct predicates.
Our encoding of the proof rule for CAS operations is somewhat involved; we give a high-level description of our approach here, and relegate the details to App. E. We focus on the more-interesting case of a successful CAS operation here. The key challenge is how to select an appropriate assertion T to satisfy the premises of the rule; while we could take T as an annotation, we observed that choosing this overlap to be as large as possible is desirable in practice (since this reduces the resources to be transferred, and which must interact in some cases with the modalities). We write out Viper code to indirectly compute this largest-possible T as follows (see Fig. 11(ii) for an illustration).
We introduce yet another heap ("tmp") in which we inhale the invariant for the value read (Q[e/V]). Now, we attempt to exhale the invariant for the value written (Q[e /V]), but adapt the assertions as follows: for each permission in the invariant, we take the maximum possible amount from our "tmp" heap; these permissions correspond to T . Any remainder is taken from the current heap (either the real or the "up" heap, depending on τ ); these correspond to P . We can express this by rewriting the assertion: for example, if τ = rlx and Q[e /V] ≡ acc(x.f, p), we exhale let p = min(perm(tmp(x).f ), p) in acc(tmp(x).f, p ) && acc(up(x).f, p − p ). If this modified exhale succeeds, any remaining permissions in the "tmp" heap correspond to the assertion A and are moved (in a way similar to our fence acq encoding in Fig. 9) to either the real or "down" heap (depending on τ ).
This combination of techniques results in an automatic support for CAS statements and their corresponding proof rule: all of the above steps can be statically generated. This completes the core of our Viper encoding, which now handles the complete set of memory access constructs from Fig. 1.

Additional Features
Our encodings support a number of additional features which, for space reasons, we describe only briefly here. Firstly, FSL++ adds rules for ghost state to the program logics; ghost locations are fictitious (non-atomic) heap locations added to a program purely for reasoning purposes. For ghost locations, the modalities collapse: if an assertion A depends only on ghost locations, then A ≡ A ≡ A holds [12]. Extending our encoding is simple; we track ghost status with an extra boolean function, and tweak our axiomatisation of multiple heaps (Sec. 4) to make the up and down functions act as identities on ghost locations (details in App. D).
Atomic read and CAS operations are often employed in simple spin loops which wait to read a particular value. Our encoding requires loops to be annotated with invariants in the general case; however, for spin loops (i.e. loops which poll a location via atomic reads or CAS operations until a certain value is read) we can avoid this, based on the following observation: every loop iteration except for the very last (and the first, for atomic reads rather than CAS operations) will make no change to the owned resources. For verification purposes, therefore, it is sufficient to encode only these iterations of spin loops, removing the need for a loop and invariant at the Viper level.
Our encoding also supports fetch-update instructions (such as atomic increment) commands natively. These differ from CAS operations in two ways: they never fail, and the updated value can be a function of the value read. Such commands could also be implemented with CAS operations and loops [12], but this is less efficient for architectures which support atomic fetch-update instructions natively.
The RSL logics do not allow resources to be directly leaked; there are no rules for weakening an assertion in order to drop unneeded permissions or assertions such as Rel and Acq assertions during a proof. Nevertheless, the RSL logics do not guarantee absence of memory leaks in general, due to the rules for atomics; there is no guarantee that resources transferred by one thread will be received by another. In order to get rid of unneeded resources, postconditions proved in the RSL logics typically include true as a conjunct, which can be used to hide unwanted assertions. (In such separation logics, true is used to represent arbitrary resources, while emp represents no resources at all). We reflect this practice by adapting the RSL rules to use true instead of emp throughout. In Viper, resources can be leaked by default, at any time. However, using Viper's perm construct, we can nonetheless encode additional checks that e.g. ownership of non-atomic locations is never leaked (we do this in our examples).

Examples and Evaluation
We have evaluated our encoding by applying it to a number of examples (described below). We are in the process of developing a front-end to generate the appropriate Viper code; for the present time, we have done so by hand. The encoded examples are available in the online appendix [3], and examples of the corresponding input source code (in the language of this paper) are given in App. B.
The encoded versions of the examples make heavy use of Viper macros, to aid readability. Each example consists of three parts: a set of background definitions (e.g. macros to encode each statement), a set of invariant definitions (specific to the example; these reflect the image of the indexing discussed in Sec. 3, along with, e.g., their . down variants), and a set of method definitions, corresponding to the translation of the original source code. The three parts are presented in reverse order (so that the source code translation comes first in each file). We also constructed some variations which exercise, e.g., the support for rewriting Acq invariants, since in all other examples our automatic support for tracking conjuncts was sufficient.
As a starting point, we took the examples from the RSL [28] and FSL [13] papers, along with variants of these to demonstrate and test additional features of the logics and our encoding. We also investigated encoding the main example from the latest FSL++ paper [12], which is the Rust reference-counting library [1]. Our encoding does not yet support all of the features of the latest paper; in particular, the proof in [12] requires customising the underlying permission model, which Viper does not directly support. Following the suggestion of one of the authors [27], we were however able to encode two variants of the original example, in which some of the access modes are strengthened, making the code slightly less efficient but enabling a proof using a simpler permission model. These variants still require counting permissions [6], which we were able to express with additional background definitions (reusable for other examples) in the Viper file.
Finally, we tackled seven functions taken from a reader-writer-spinlock in the Facebook Folly library [2]. We were able to verify five of the functions simply, but two employ code idioms which seem to be beyond the scope of the RSL logics, at least without sophisticated extra ghost state; the code depends on the combined effects of multiple atomic accesses (rather than reasoning about these individually). For both functions, we developed alternative implementations which could be handled by the logics and our encoding.
Our initial experiences with our encoding are promising; verification times were reasonable (generally around 10 seconds, and always under a minute), and several errors in our initial specification attempts (and typos in the example code) were uncovered during their development. It is clear that a front-end for our encoding will be necessary to evaluate it on larger codebases; this is largely a matter of engineering (and good error reporting!), and is already underway. The Rust and Facebook code quickly demonstrated a key advantage of working in a general verification framework such as Viper; both examples required support for extra theories (counting permissions as well as modulo and bitwise arithmetic), and we were able to plug such reasoning in simply.
In terms of annotation overhead, the examples require specifications (the ratio between specifications and code was roughly 1:1 for each example tackled so far), but no manual assertions (e.g., between statements) were necessary to make the verification go through. The overhead involved compares favourably to a manual proof effort; an informal proof outline would also require (at least) one intermediate assertion between every statement, while the Coq-formalised proofs of the first RSL examples require roughly ten lines of proof per line of code [28], and for the Rust reference counting example (albeit for an unmodified version of the implementation; i.e., a harder proof), the ratio is around 100:1. Such ratios are consistent with other recent Coq-mechanised proofs based on separation logic [29].

Conclusions and Future Work
We have presented the first encoding of modern program logics for weak memory models into a deductive program verifier. The encoding enables programs (with suitable annotations) to be verified automatically by existing back-end tools. Moreover, the encoding into a general-purpose verification framework such as Viper provides additional features such as theory reasoning in a straightforward manner (an example being the counting permissions used in our reference counting proofs).
Building front-end verifiers for such advanced logics will enable them to be applied to more, and larger, examples; we are currently working on building a front-end for the translation presented here. In future work, we plan to prove the soundness of the presented encoding; a key ingredient will be the mapping back from a Viper program state to the analogous program state in the RSL logics.
We are interested in tackling other modern program logics such as GPS, which also handles weak memory. We believe that many of the techniques described in this paper can be carried over, and applied to build practical tools that implement these advanced logics. The quick feedback provided by such tools will also enable us to identify weaknesses in the logics themselves, inspiring further developments.

A Full Source Encoding
The encoding of the general source language of assertions is given below (we assume the encoding of pure expressions, which can typically be the identity mapping, assuming all operators supported such as addition, equality etc. are all supported natively by Viper).

C Rewriting Invariants
It is unusual (at least, in the examples we have investigated so far) for very many different invariants for atomic locations to be needed; it is even less common for there to be a need for many different invariants for the same atomic location. Indeed, for Rel and RMWAcq assertions, since the assertions are duplicable, one may as well always use the same invariant. For Acq assertions the situation is more interesting; it may be desirable to split the invariant (as used e.g. in Fig. 15) across several Acq assertions, and programmer-annotated assertions may not always syntactically match up precisely (since there might be more readable ways of expressing an equivalent assertion). Since our indexing of Acq invariants matches their conjuncts syntactically, additional work is required if this syntactic match would be overly restrictive. For example, in the example shown in Fig. 15, the initial Acq invariant is expressed more succinctly in a way which does not provide the immediate conjuncts needed by the (left and  right) forked threads. In such cases, we support an additional rewrite statement rewrite Acq(l, Q) as Acq(l, Q ) in the source program to explain the intended rewriting. To check that such a statement is justified, we need to check the entailment between the original and new invariants, for all values of V. Furthermore, this entailment check cannot be made directly in the current state, since that might already contain permissions and value information which could unsoundly weaken the check made, or even contradict the invariants involved, resulting in an infeasible program state from there onwards.
To avoid these issues, we perform the following steps (shown in Fig. 17). For simplicity, we do not handle the case of rewriting invariants for which values have already been read (we check that this is not the case, here, but an extension is possible). Firstly, we create a non-deterministic if-branch. Inside the branch we remove all permissions from the current state. We then havoc an integer variable, representing the arbitrary value of V for which the subsequent check should hold. We inhale the original invariant (using our indexing as usual), and exhale the invariant to which it is to be rewritten. If this succeeds, we know that the rewriting is justified; the one invariant entails the other, for all values of V. We then kill this branch, by adding an assume false to it; subsequently, only the other branch (in which no changes were made) will be considered for verification.
Lastly, we perform the rewriting itself by discarding all of the original AcqConjunct instances, and replacing them with the new ones. Verification can then proceed as usual.

D Ghost Locations
We extend our encoding to handle ghost locations in a simple manner. Firstly, we add a boolean function is / _ ghost on references, to identify whether or not a location is ghost. We added macros realRef(r) to add the appropriate assumptions for a real program reference, and ghostRef(r) for ghost locations; this can be seen as the translation of "type information", since we assume that ghost locations are labelled as such in the original program.
For ghost locations we tweak our multiple heaps encoding to change the assumptions about the up and down mappings to instead require them to act as the identity (correspondingly, the result of heap is no longer constrained to be different after applying these mappings to a ghost reference). This immediately gives us that, for assertions depending only on ghost locations in the heap, A, A and A will be handled equivalently; since (up to syntactic applications of these identify mappings) they will be encoded as identical assertions.
Finally, we add an assumption of realRef(r) to our existing statements for allocating references, and add a new ghost allocation statement, for which the analogous ghostRef(r) assumption is added. The most-relevant details are summarised in Fig. 18.

E Compare and Swap Details
The details of our encoding of the FSL++ compare and swap rules (cf. Fig. 10) are shown in Fig. 19. We represent RMWAcq assertions similarly to Acq assertions (cf. Fig. 7), but and a false value of the acq field to differentiate holding one from the other. Recall that we must choose at allocation whether atomic reads or compare and swaps will be used to gain ownership via the atomic location; this choice is then reflected in the field value. The encoding of allocation is then straightforward.
The handling of a CAS statement itself involves initially checking that we indeed hold some Init, RMWAcq and Rel() assertions for the location, according to the precondition of the rule, and then using an if-condition over the fresh read value x to narrow us down to the case of a successful CAS. The subsequent Viper code reflects the three steps described in Sec. 5 and Fig. 11. Firstly, we perform the inhale of newly-gained resources (corresponding to Q[e/V]) into the tmp heap.
Secondly, we attempt to exhale the assertion Q[e /V], modified so that the permissions are taken preferentially from the tmp heap, and failing this, from the real heap or up heaps, depending on whether or not the write synchronises. This modification of the assertion (which splits the permission amounts across the two heaps, as described in Sec. 5) is denoted by the . tmp/real and . tmp/up mappings; if the values of heap locations are also mentioned in the parameter assertions, then these heap dereferences must also be rewritten to a dereference in the corresponding heap (e.g. x.val == 4 might become tmp(x).val == 4). In case permission to the corresponding location is taken partly from both heaps,  the extra assumption that the two values are the same can be explicitly added by these mappings.
Finally (assuming the exhale has succeeded, otherwise a verification failure will have been encountered), all remaining permissions in the tmp heap are transferred to either the real or down heap, depending on whether the read synchronises.