Leaf: Modularity for Temporary Sharing in Separation Logic

In concurrent verification, separation logic provides a strong story for handling both resources that are owned exclusively and resources that are shared persistently (i.e., forever). However, the situation is more complicated for temporarily shared state, where state might be shared and then later reclaimed as exclusive. We believe that a framework for temporarily-shared state should meet two key goals not adequately met by existing techniques. One, it should allow and encourage users to verify new sharing strategies. Two, it should provide an abstraction where users manipulate shared state in a way agnostic to the means with which it is shared. We present Leaf, a library in the Iris separation logic which accomplishes both of these goals by introducing a novel operator, which we call guarding, that allows one proposition to represent a shared version of another. We demonstrate that Leaf meets these two goals through a modular case study: we verify a reader-writer lock that supports shared state, and a hash table built on top of it that uses shared state.


INTRODUCTION
Multi-threaded concurrent programs are difficult to get right.One challenging pattern in such programs is read-sharing, i.e., allowing multiple threads to simultaneously read mutable shared state as long as no other thread is actively writing.This common optimization reduces thread contention and is often considered critical for scaling concurrent performance.While the general idea is commonly deployed, the concrete instantiations vary wildly.For example, even the implementation of a conceptually simple reader-writer lock grows quite complicated as various kinds of scaling issues are considered [Calciu et al. 2013;Dice and Kogan 2019;Guerraoui et al. 2019;Hsieh and Weihl 1992;Kashyap et al. 2017;Liu et al. 2014;Shirako et al. 2012].As a concrete instance, one possible optimization uses multiple reference counters, each on its own cache line, to reduce thread contention for readers [Calciu et al. 2013].And yet the challenge does not stop at reader-writer locks; for instance, the node-replication algorithm of Calciu et al. [2017] might allow simultaneous read-access to particular entries of a ring buffer, and this protocol does not resemble a lock in the slightest.Given all of this complexity, we would naturally like to verify, in a modular fashion, both the read-sharing implementations and the programs that use them.
One effective tool for reasoning about concurrent programs is concurrent separation logic (CSL) [O'Hearn 2007;Reynolds 2002] which lets us reason naturally about exclusive ownership of memory and, more generally, arbitrary resources.This is a great fit for (mutually exclusive) locks: it is easy to write a specification of a lock that allows the client to obtain ownership of a resource (e.g., permission to access some part of memory, or a more complicated invariant) which is returned upon releasing the lock, allowing the resource to be transferred between threads.
But how do we handle simultaneous read-sharing?We want some way of reasoning about this simultaneous, shared access-e.g., we want to talk about memory access where we can only read but not write, or invariants which must be preserved until the shared access is released.CSL's exclusive ownership does not work here, since the desired type of ownership is not exclusive.Some more recent CSLs also have a concept called persistent knowledge [Bizjak and Birkedal 2018], which allows indefinite sharing, but this on its own does not suffice either: whatever sharing mechanism we use needs a way to reclaim exclusive access once all shared access has been revoked.Two of the earliest proposed methods to represent shared state while allowing reclamation are fractional permissions [Boyland 2003] and counting permissions [Bornat et al. 2005].These representations have the benefit of being easy to construct and prove sound, but they do not form complete proof strategies for complex sharing protocols like the above, which are likely to have considerably more state that evolves in complex ways and cannot be expressed through fractional permissions or reference counters alone.Furthermore, the desire for modularity means there is pressure for specifications to converge on one particular representation so that they can interoperate; this limits flexibility since different representations might be more suitable for different applications.How can we support a wide gamut of sharing techniques-so the user can choose the right tool for the job, even building their own representation if necessary-while also achieving modularity?
Our approach is to take the core idea of fractional and counting permissions, generalize it, and a provide a uniform way to reason about it.That core idea, we argue, is that both involve propositions which are able to "stand in" for some kind of shared resource, but which are suitable for manipulation within the substructural separation logic.This enables them to handle the temporality of the sharing.This motivates us to extract the essence of this "standing-in" relationship and brings us to our contribution, the Leaf logic.
Leaf introduces a novel operator ⤔, which we call guarding, to represent the relationship between an exclusively owned proposition and the shared proposition it represents.Leaf handles both sides of this abstraction.First, we show how the user can deduce nontrivial ⤔ relationships by constructing arbitrary sharing protocols.Such protocols include ones based on the above-mentioned patterns, as well as custom protocols that are tailored to particular implementations.Our approach is inspired by custom ghost state [Dinsdale-Young et al. 2013, 2010;Jung et al. 2015;Krishnaswami et al. 2012; Ley-Wild and Nanevski 2013;Nanevski et al. 2014], a class of flexible separation logic techniques.We call the protocols of our new formulation storage protocols.Second, we show how to make use of ⤔ relationships, through general rules that are agnostic to the underlying sharing mechanism, thus enabling modular specifications.This approach is symbiotic between ghost state and readsharing: by applying custom-ghost-state techniques, Leaf supplies a general form for read-sharing mechanisms; meanwhile, some ghost-state constructions become simpler because they can rely on Leaf's read-sharing, without needing to include their own bespoke sharing mechanisms.
Storage protocols can support complex algorithms found in real-world concurrent software systems.As we discuss in §6, Leaf's storage protocols have already been used in the IronSync framework [Hance et al. 2023], which is enabled by Leaf's systematic approach to read-shared custom ghost state.IronSync targets production-scale, high-performance concurrent systems, e.g., a multi-threaded page cache (reaching 3M ops / second) or the node-replication algorithm mentioned above (3M ops / second across 192 threads).These results confirm that high-performance applications contain sophisticated, domain-specific read-sharing patterns; demonstrate that Leaf's storage protocols can handle them; and provide evidence that system developers find Leaf's perspective on temporarily read-shared resources useful.In this paper, though, we will mostly focus on the technical formalism of this perspective, so we use a smaller, self-contained example that is simple enough to explain in full, but still complex enough to show the utility of Leaf.
Specifically, we make the following contributions: • We present Leaf, which has built-in deduction rules for temporarily shared state and a mechanism for user-defined sharing protocols based on ghost state, which we call storage protocols.• We show how storage protocols capture existing patterns, e.g., fractional and counting permissions.
• We illustrate Leaf's modular specifications through a case study of a reader-writer lock and a hash table, demonstrating several different facets of sharing: -The hash table itself is shared between threads.
-The hash table is composed of many reader-writer locks, which inherit that sharedness.
-The memory cells in the reader-writer lock further inherit that sharedness, and they are accessed atomically by multiple threads.-The reader-writer locks allow temporary, shared read-only access to the hash table's memory slots.Furthermore, this is done via a clean, modular specification of the reader-writer lock.• We prove the soundness of Leaf and provide it as a library in the Iris framework [Jung et al. 2018] in Coq. 1 We mechanize our case studies in Coq.These proofs are available as open source and in our supplementary materials.

OVERVIEW
Leaf is constructed as a library in the Iris separation logic [Jung et al. 2018].We generally use Iris notation, where applicable, and all the standard Iris proof rules apply.We assume familiarity with separation logic basics (the connectives * , − * , and so on), but we will review key Iris features as they come up.

Leaf Introduction: Resource Guarding
The primary question we need to unravel is how to talk generally about "a shared " for any proposition .Here, the proposition  might be something simple, like the permission to access a certain memory location ℓ and read a specific value , denoted ℓ ↩→ , or it might be a more complex invariant.In order to make shared state reclaimable, we connect the "shared " to some exclusively owned (not persistent) proposition.
We do this via a relationship  ⤔ , pronounced  guards . ⤔  is itself a proposition; informally, it means that  can be used as a "shared ."Hence, if some program proof needs to operate over a "shared ," it can instead take  as an exclusively owned precondition, and use the relationship  ⤔  when it needs to use .Later,  might be consumed (disallowing further shared access to ), and eventually the exclusive ownership of  might be reclaimed.
In general, then, when we want to write a proof that operates over some read-only  in a way that abstracts over the way  is shared, we can write the proof to take ownership of some arbitrary Iris proposition  : iProp where  ⤔ .To codify this pattern, we use a shorthand, [ ] { }  { }, to mean ∀ : iProp.{ *  * ( ⤔  )}  { *  }.This can be read as "if command  executed, with  owned at the beginning, and with  shared, then  is owned at the end." We indicate shared resources in purple, though this is only a visual aid, and it has no syntactic meaning.
For example, a program logic might allow writing to a memory location given exclusive ownership of ℓ ↩→ , but allow reading from it given shared ownership of ℓ ↩→ .Leaf specifies this as: Here, ℓ ←  ′ is the command to write to the reference ℓ, while !ℓ reads it.A bound variable in a postcondition, e.g. here, represents the command's return value, so Heap-Read-Shared says, if we have a shared ℓ ↩→  and read from ℓ, then we obtain a value equal to .Fig. 1.Example specification for a reader-writer lock using Leaf notation.In §4, we show how to prove this specification for a particular implementation.

Example: A Reader-Writer Lock Specification
The reader-writer lock spec in Figure 1 illustrates several facets of our guarding system.The API of this lock has six functions: rwlock_new and rwlock_free are the constructor and destructor, respectively; lock_exc and unlock_exc are intended to allow exclusive, write access to some underlying resource; lock_shared and unlock_shared are intended to allow shared, read-only access.Exactly what this "resource" is may be determined by the client.
Holding the spec together is the proposition IsRwLock(rw, ,  ), which roughly says that the value rw is a reader-writer lock with a unique identifier . is used to specify the resource being protected-we will return to this in a moment.Note that when a new reader-writer lock is constructed (via rwlock_new) the client obtains exclusive ownership over IsRwLock(rw, ,  ); on the other hand, the operations that are meant to run concurrently all take IsRwLock(rw, ,  ) as shared.The destructor, rwlock_free, again requires non-shared ownership, as naturally it should not be able to run concurrently with other operations.Now, the client needs to specify what sort of resource they want to protect.For example, the client might want to protect access to some location in memory, say ℓ, so they would use the lock to protect resources of the form ℓ ↩→ .To allow the client to choose the kind of resource they want to protect, our specification lets the client, upon construction of the lock, provide a proposition family  :  → iProp parameterized over some set  .In the above example, we might have  =  : Value.ℓ ↩→  for some fixed ℓ determined at the time of the rwlock_new() call.
In the specification, observe that we then use  (), for some , to represent the resource when it is obtained from the lock by lock_exc.Upon calling unlock_exc, the client then has to return some  ( ′ ), where  ′ might be different than .This makes sense, because lock_exc is supposed to be a write-lock, so the client should be able to manipulate the given resource at will, provided it restores the lock's invariants.
Acquiring the shared lock is more interesting, since we have to acquire some  () resource in a shared way.This is where the ⤔ operator comes in: rather than receiving  () directly, the client obtains a special resource Sh(, ) (for some ), for which we have Sh(, ) ⤔  ().Thus, the client has shared access to  () as long as it has the Sh, which must be relinquished upon release of the lock.We view Sh as a separation logic analogue of a lock guard, an object in some locking APIs [The cppreference Team 2011; The Rust Team 2014] which exists for the duration of a held lock.Indeed, this inspires the guarding name.
Notice the choice of parameter set  and proposition family  determines exactly what it means to be "read-only, " because it is the value  :  which is fixed until the client releases the shared lock.For example, we might set  = Z and  =  : Z. ℓ ↩→ .Then the client can take a shared lock Fig. 2. Fractional and counting permissions expressed by the ⤔ operator.The ⇚⇛ means we can perform an update to exchange exclusive ownership of one side for the other, while ⊣⊢ means both sides are equivalent.Frac and Count are arbitrary namespaces ( §3.4).In Leaf, these laws are derived from storage protocols ( §3.4).
and obtain shared ℓ ↩→  for some fixed integer , which cannot change until the lock is released.On the other hand, they might set, say,  = Z 2 and  =  : Z 2 .∃. (ℓ ↩→ ) * ( =  mod 2).In this case, upon taking the shared lock, they receive ℓ ↩→ , but now only the parity of  is fixed.In fact, in this situation, the user would be able to update  to another value of the same parity (provided they do so in an atomic operation).
The RwLock spec raises two questions: How can the client do interesting things when they have Sh(, ), i.e., a "shared  ()", rather than exclusive ownership of  ()?Secondly, how can we verify a realistic lock implementation against this spec, which requires the deduction of a nontrivial guard relationship Sh(, ) ⤔  ()?Let us tackle these in turn.

Utilizing Shared State
How does the user actually benefit from shared state, i.e., state (like  ()) under a guard operator, as in (Sh(, ) ⤔  ()) from the previous example?
In general, if  ⤔ , Leaf aims to let  be usable in any operation that could have used , provided that  is not modified.Such an operation might be given by the Iris operator called the view shift, as in  *  ⇛  * .In general, the view shift (⇛) effectively says we can give up the resources on the left side to obtain the resources on the right.In the example, though, with  on both the left and right sides,  is not consumed, although it is needed to perform the operation.In this case, we could use  in place of ; i.e., we would have  *  ⇛  * .
Frequently, in order to perform such updates, we need to first compose multiple pieces of shared state together; for example, suppose we employ a fine-grained locking scheme, where a thread might hold multiple pieces of state from different locks in shared mode, which all together are needed to perform a certain update or deduction.Ordinarily, we would compose the corresponding propositions with separating conjunction ( * ), but here, the pieces, being shared, might come from the same source and not actually be separated.To get around this, we use overlapping conjunction (∧) rather than * when dealing with shared state.It turns out that constructing sound deduction rules to use ∧ is subtle; the rule we give in §3.2 requires a specific technical condition.We will show how all this works together through our fine-grained hash table example ( §5).

Deducing Guard Relationships
Towards verifying an implementation of a reader-writer lock, the most salient technical question is how we can construct nontrivial propositions like Sh(, ) and prove guard relationships on them.To tackle this question, it helps to first look at simpler examples of nontrivial guard relationships.As such, let us take a look at fractional permissions [Boyland 2003] and counting permissions [Bornat et al. 2005], two of the oldest known methods used to account for reclaimable read-shared permissions for memory (and other resources).

Fractional and Counting
Examples.In the fractional paradigm, the points-to proposition is labeled with a rational number  : .These propositions combine additively: (ℓ , where the ⊣⊢ is bidirectional entailment, i.e., the two sides are equivalent.Write permission is given by ℓ frac ↩− − → 1  and read permission is given by any ℓ frac ↩− − →   where  > 0. The idea is that the ℓ frac ↩− − → 1  can be split into multiple fractional pieces, which can be handed out and used in a read-only fashion, and then put back together to obtain write access, allowing the user to change .Intuitively, the reason this works is that one cannot reclaim write access without gathering all the read-only pieces, since all of them are needed to sum back to 1. Thus anyone holding onto a read-only piece cannot have the value changed out from under them by another thread. Counting permissions, on the other hand, does not allow arbitrary splitting, but instead uses a centralized counter, ℓ count ↩− −− →   ( : N) to keep track of the number of extant read-only permissions, denoted ℓ ro ↩− → .The user can increment the counter to obtain another read-only permission, or perform the inverse: gives write permission; i.e., we can write as long as there are zero read permissions in existence.
In Leaf, we can express both these patterns using ⤔, as shown in Figure 2. The idea of the "write permission" is expressed by saying that ℓ frac ↩− − → 1  can be exchanged for ℓ ↩→ , and vice versa; the "read permission" is expressed by the guards relationship: (ℓ frac ↩− − →  ) ⤔ Frac (ℓ ↩→ ).(We explain the Frac label later.)The same approach works for the counting permissions.
Note that with this setup, we do not need to prove the heap read and write rules for the fractional and counting permissions individually.Rather, we simply apply the more general Heap-Write and Heap-Read-Shared rules from earlier along with any guard relationship, such as one from Figure 2. All of these can be constructed by via a Leaf formalism called a storage protocol, so named because they allow the user to "store" propositions (e.g., by (ℓ ↩→ ) ⇛ Frac (ℓ frac ↩− − → 1 )) and then access them in a shared manner.The core idea is based off of custom ghost state, a concept wherein a user may define their own resources and derive update relations (⇛).Storage protocols extend the concept to also allow the derivation of guard relations (⤔).
The propositions constructed by the protocol are able to guard arbitrary propositions that have no intrinsic notion of being shareable.For example, in the fractional example, ℓ ↩→  has no notion of being shareable; rather, given ℓ ↩→ , without knowing anything about its definition, Leaf allows us to construct the ℓ frac ↩− − →   proposition with a particular guard relationship to ℓ ↩→ .This is an instance of the same feature that lets us have Sh(, ) ⤔  () parameterized by an arbitrary proposition family  .

Outline
Throughout the paper, we explore these examples in more detail.We first formally introduce ⤔ and its elementary deduction rules, and sketch how we can derive rules like Heap-Read-Shared within Leaf.We then introduce our new formulation of custom ghost state, allowing the deduction of nontrivial ⤔ propositions, such as those in Figure 2. We show how to verify a reader-writer lock, proving the specification (Figure 1) holds for a particular implementation.To illustrate Leaf's modular specifications, we then build another application on top of the reader-writer lock.Finally, we discuss our construction of Leaf within Iris and the definition of ⤔, proving our laws sound.

THE LEAF LOGIC
We begin our presentation by reviewing the concept of custom ghost state.The formulation we present first is (largely) standard, yet still significant within Leaf.Then we will dive into Leaf's ⤔ operator and our new extension of custom ghost state.

Custom Ghost State (Background)
Custom ghost state in Iris is a mechanism through which the user can soundly construct their own resource with custom update rules.We present a simplified version of it here, based primarily on the "Iris 1.0" formulation [Jung et al. 2015].
The construction is parameterized by a partial commutative monoid (PCM) whose elements form the basis of the resource.Formally, a PCM is a set  (the carrier set) with a composition operator • :  ×  →  which is associative and commutative, and with a unit element .We let  ⪯  ≜ (∃. •  = ).The partiality is represented by a validity predicate V :  → Bool, where V () and ∀, . ⪯  ∧ V () ⇒ V ().We also define a derived relation ⇝ called the frame-preserving update, Essentially,  can transition to  if for any valid way of "completing" state, the state would remain valid after the transition.
For any such , Iris shows the rules in Figure 3 are sound for a proposition written

𝛾
for  : .The  : Name is a ghost name (sometimes called ghost location) from an arbitrary, infinite set of available names.These rules show, for instance, that the compositional structure (•) of the monoid determines the compositional structure within the logic, i.e.,  as given by PCM-Update.The operator ⇛ is called view shift, and it essentially means we can give up the resource on the left-hand to obtain the resource on the right.VS-Hoare says we can perform such updates at any program point during a proof.Note that the view shifts can also be annotated with a mask, denoted ⇛ E ; we discuss this further in the next section.
Example 3.1.An archetypal PCM is the exclusive monoid, Excl( ), for a given set  .The elements of Excl( ) are made out of the following symbols: .Then we can conclude that ex() ∧ ex()  ⊢ ( = ).

The Guarding Operator and its Elementary Deduction Rules
The fundamental building block of the Leaf logic is the ⤔ operator, pronounced guards.When we write  ⤔ E  , we call  the guard and  the guarded proposition or sometimes the guarded Rules for PCM ghost state Instantiated for a given PCM (, •, V) Propositions:   (where  : ,  : Name) Deduction rules for guarded resources Persistent Propositions:  ⤔ E  (where ,  : iProp, E : (Name)) invariant, and this means that having  allows shared access to  .Guards, like view shifts, are annotated with a mask E, as we discuss below.The basic rules for ⤔ E are given in Figure 4.
For example, Guard-Refl says that a  represents a shared , while Guard-Trans says that if  is a shared , then a shared  is also a shared .Guard-Pers and Unguard-Pers show how persistent propositions can move into or out from under a ⤔.Guard-Upd says that when an update  *  ⇛  *  is valid, then we can perform this update even when we have a shared .
Mask Sets.We use mask sets, E, to track the "sources" of the sharing for both guard relations (⤔ E ) and view shifts (⇛ E ).The sharing sources-as we will see later-are called storage protocols, and each storage protocol has a name .A mask is a set of such names, and it can be considered an over-approximation of the set of storage protocols involved in the derivation of a given relation.(In §7, we will see that this interpretation of ⇛ E follows from its usual Iris definition.) We need to track these because we need to be careful when we apply Guard-Upd.If we applied Guard-Upd twice while ignoring its disjointness condition, we could potentially "double up" a proposition that is shared between multiple guard objects.For example, if we had  1 ⇛ E  and  2 ⇛ E , we could use  1 *  2 to perform an update that would only be possible if we had  * .The disjointness condition prevents us from doing this.Those familiar with Iris may observe that this is similar to invariant reentrancy, so it should be no surprise that we solve the problem the same way, that is, via mask sets.
Guards and implications.One might expect a rule where we use  ⤔  and  ⊢  to conclude  ⤔  .This does work when we can write  =  *  ′ for some  ′ (Guard-Split), but it does not hold in general: consider, for example, a judgment such as (ℓ ↩→ 1) ⊢ (∃ .ℓ ↩→ ).It would be unsound if a user sharing ℓ ↩→ 1 could "downgrade" it to the right-hand side; that user could then update ℓ to a different value and invalidate the proposition the other users were relying on.
Interestingly, it turns out that there are some propositions  such that any judgment  ⊢  can always be "split" into  =  *  ′ .Specifically, this happens whenever  is of the form   or a conjunction thereof.We call these point propositions and indicate them by point( ) (PointProp-Own, PointProp-Sep).For such  , we can indeed conclude  ⤔  (Guard-Implies).
Overlapping Conjunction.How can we compose shared state?We certainly cannot have a rule like . After all,  and  might be shared from the same source and thus not be properly separated.Somewhat surprisingly, this rule is not even sound if we require the masks to be disjoint.(See Appendix B.4 for a concrete counterexample.) Instead of using * , we use ∧.One might instead conjecture a ∧-based rule like ; this rule still is not sound on its own (again, see Appendix B.4 for a concrete counterexample), but fortunately, it becomes sound as long as we add another point proposition condition (Guard-And).This rule is especially useful in combination with PCM-And, which can be used to deduce the premise of Guard-And.

Using ⤔ in a program logic
Deriving heap rules.Iris is not a separation logic for a single programming language; rather, it is a general separation logic framework which can be used to instantiate a program logic for any user-provided programming language.In other words, rules like the following, which might be considered "primitive" rules within a program logic, can actually be derived soundly within Iris.
Let us overview this process, and then explain how it works with Leaf's ⤔ in the picture.
To instantiate a program logic, the user provides their programming language and its operational semantics.Here, we consider heap semantics operating over a state given by  : Loc fin − ⇀ Value, with allocation (ref), deallocation (free), assignment (←) and reading (!).Next, the user gives meaning to the heap state  within the separation logic by defining an interpretation of the heap state as a proposition, H () : iProp, along with propositions to be manipulated by the user within the program logic (here, ℓ ↩→ ).Finally, they prove the primitive heap rules via corresponding updates or entailments.For example, the following suffice to show the above four Hoare rules.
Thus, it suffices for the user to construct H () and ℓ ↩→  so that the above hold; this can be done via a custom PCM construction, using PCM-Valid to prove ReadEq, PCM-Update to prove WriteUpd and FreeUpd, and PCM-Alloc to prove AllocUpd.Now, the new rule we want to construct is, for any ℓ, , E, E 1 , Expanding the notation, this is equivalent to, for any  : iProp, This follows from, and this in turn follows from Unguard-Pers and ReadEq.Notably, we do not need to re-do the construction of H () or ℓ ↩→  to support the derivation of Heap-Read-Shared.Along with the new deduction rules for ⤔, the old construction "just works." Atomic Invariants.Propositions shared via guarding can serve as atomic invariants; i.e., we can obtain exclusive ownership of a shared proposition for the duration of an atomic operation, as long as we restore the invariant at the end of the operation.
Non-Atomic Memory.The heap semantics in the preceding example use sequentially consistent, atomic heap operations.But what about other memory ordering models?
We can also apply Leaf to heap semantics that model non-atomic memory access, i.e., memory accesses for which data races are entirely disallowed, alongside atomic operations.Non-atomic memory has been modeled before in Iris, e.g., by RustBelt [Jung et al. 2017], which models each non-atomic operation as two execution steps in order to detect overlapping operations.We can apply Leaf to this situation, and prove Heap-Read-Shared for non-atomic reads; however, the proof is slightly more challenging than it is for the purely-atomic heap semantics, primarily because the heap semantics have to model non-atomic reads as effectful operations.To get around this, we need to be slightly clever in our definition of H (); see Appendix G for a sketch.

Storage Protocols
Leaf's storage protocol is a formulation of custom ghost state whose unique feature is its laws allowing deductions of nontrivial ⤔ propositions.Storage protocols are similar to the ghost state presented earlier, which embeds elements of a monoid as separation logic propositions   and uses a derived relation (⇝) to deduce updates (⇛).The storage protocol formulation, however, is given as a relationship between two monoids, a protocol monoid  and a storage monoid .Elements  :  are embedded as propositions ⟨⟩  , analogously to   , while elements  :  are embedded via an arbitrary proposition family  :  → iProp, which is specified upon initialization of the ghost state (SP-Alloc).The update relation and the resulting ⇛ propositions are complicated by the need to account for , and we also include a new derived relation, ⇸, from which we deduce guards of the form ⟨⟩  ⤔ E  ().Figure 5a gives the precise definitions for these derived relations.
Let us first discuss the role of , and its impact on our definition of updates. and  are related by a storage function S :  → , which intuitively associates to each possible state of  some value  :  that is "stored, " and since the stored value might change upon any update, we need to account for this in the definition of updates.For example, when the -state updates, the corresponding stored state (S()) might also change; if the stored state changes from  to  • , we consider  to be "deposited"; if the state changes the other way, we consider it withdrawn.The most general form of an exchange is given by ⇝ , with the other three as special cases: A withdraw ( ⇝) occurs when "nothing" (i.e., the unit) is deposited, a deposit ( ⇝) occurs when nothing is withdrawn, and a "normal" update (⇝) occurs when the stored value remains constant.These relations then give rise to updates in the separation logic (SP-Exchange, and its special cases, SP-Deposit, SP-Withdraw, A storage protocol consists of: A storage monoid, that is, a partial commutative monoid (, •, V), where, A protocol monoid, that is, a (total) commutative monoid (, •), with an arbitrary predicate C :  → Bool and function S :  | C →  (i.e., the domain of S is restricted to the subset of  where C holds) where,

Derived relations for storage protocols:
For ,  ′ :  and ,  ′ : , define: Storage Protocol Logic Instantiated for a given storage protocol (, •, V), (, •), C, S Propositions: ⟨⟩  Persistent propositions: sto(,  ) (where  : Name,  :  → iProp,  : ) SP-Update), operating on ⟨⟩  and  ().Specifically, a deposit of  allows an update that gives up ownership of  (), while a withdraw of  allows an update that obtains ownership of  ().Now, with the ability to "deposit" elements into the protocol and "withdraw" them, we can add the ability to have shared access to those stored elements: the derived relation  ⇸  gives rise to ⟨⟩  ⤔  () by SP-Guard.Let us unpack the definition of ⇸ to understand intuitively why this should work:  ⇸  is defined as ∀.C( • ) ⇒  ⪯ S( • ); this essentially says that  is a "witness" that the value  is stored; i.e., if we have ownership of , then any "completion" of the state  • , which is valid according to the validity function C, must be storing something ⪰ .
Initialization of a protocol.When initializing a protocol (SP-Alloc) we get to specify  , and we also get to specify the initial element  :  while giving up  (), the initial proposition to be stored.Inheriting yet another trick from Iris, we can also specify a namespace N , a subset of Name, that the resulting protocol name has to be in.Using fixed namespaces (such as Frac or Count from Figure 2) is often more convenient than managing individual names  that cannot be known a priori.Example 3.2 (Fractional protocol for a single proposition).To start simple, let us suppose we have a single proposition, , we would like to manage.Set the protocol monoid  ≜ Q ≥0 and the storage monoid  ≜ N, with composition as addition in both cases and a unit of 0. Set C to be true exactly on the integers, and for integers , set S() ≜ .Let V always be true.Now, we have the exchange (1, 0) ⇝ (0, 1) (also written as a withdraw, 1 ⇝ (0, 1)) and the reverse, (0, 1) ⇝ (1, 0) (also written as a deposit, (0, 1) ⇝ 1).Finally, for any  > 0, we have  ⇸ 1.This is the key property that says the fraction  can act as a read-only element, and it follows from the following argument: if  ′ ≥  and C( ′ ) holds, then  ′ is an integer and S( ′ ) =  ′ ≥ 1.
Finally, set the proposition family  () ≜ *  , i.e.,  conjoined  times.Now we can say that, Example 3.3 (Fractional memory permissions).Assume points-to propositions ℓ ↩→  are given.We wish to construct ℓ frac ↩− − →   and the laws as given in Figure 2. We take Instantiate the protocol to obtain a location  ∈ Frac, and then set From here we can derive the appropriate withdraw, deposit, and guard.

Handling the later modality ⊲
Note that some rules in Figure 5b use the later modality, ⊲, a feature of step-indexed logics like Iris.In Leaf, ⊲ allows us to dynamically specify the proposition families  during protocol initialization (SP-Alloc); without ⊲, this would be unsound.If we gave up that ability and instead specified all families a priori, we could remove ⊲ from the rest of the rules.(This is analogous to Iris requiring ⊲ for dynamically allocated invariants.)Leaf provides rules to eliminate ⊲ from within guards: Timelessness [Krebbers et al. 2017] is a technical condition that effectively says a proposition is "independent of the step-index," which makes it easier to account for ⊲ modalities.Timelessness holds for both the PCM-based   and our ⟨⟩  propositions.(Technically, this is because PCMs are special cases of "discrete CMRAs.") However, sto(,  ) is not timeless, since it depends recursively on iProp, but since it is persistent, we can use Later-Pers-Guard for such propositions.Fig. 7. Example execution of two threads using a shared reader-writer lock.This is an "ideal" execution, without contention or retries.First, we see Thread 1 acquire exclusive lock.This gives them exclusive control over the resource, which they can therefore modify (here, changing it from  to ) before releasing the lock.
We then see both threads acquire a shared lock, where they simultaneously have read access to the  resource.
On the left, we annotate each step with the ghost resource update from Figure 8 it corresponds to.

RWLOCK EXAMPLE: VERIFYING A CUSTOM PROTOCOL FOR SHARING STATE
Our two case studies are arranged to show the two "halves" of Leaf: the first one (this section) demonstrates the verification of a sharing protocol that lets the client acquire shared state, while our second one ( §5) shows how a client can make use of the shared state.
Specifically, in this section, we verify a reader-writer lock, one which is slightly more complicated than that which is captured directly by a standard permission logic, a situation which the storage protocol was designed for.The implementation of our reader-writer lock in shown in Figure 6, with an example execution trace in Figure 7.The implementation's main complication here is the fact that acquiring a lock is a two-step process: a thread might increment the reference counter, but then fail to acquire the lock in the second step.Hence, the physical value of the reference counter may not match the number of extant read-references.
Initially, this design might seem strange-why not just put all the data in a single atomic field to simplify the design?However, the use of distinct fields is an essential element of more complex lock designs, such as the multi-counter design mentioned in the introduction, where each counter goes on a different cache line.In fact, simply having an intermediate state at all captures most of the complexity of the multi-counter design, so we use the single-counter implementation here-see Appendix E for details on the multi-counter case.
Proof Overview.We tackle the proof in two stages: first, we devise some useful ghost resources; then, we use those resources in the program logic to prove the implementation meets the specification (Figure 1).The key is to find the right resources and their relationships that we need.
The RwLock specification (Figure 1) already indicates three propositions that we need to construct in one way or another: IsRwLock(rw, ,  ), Exc (𝛾), and Sh(, ).We also have Sh(, ) ⤔   () as a desired property.This gives us a reason to use a storage protocol: it allows us to construct new resources and derive ⤔ relationships.Since the storage protocol and its resulting resources are more elaborate than in the previous examples, we will take the time here to explain exactly how to come up with the protocol.
First, we naturally need a component to represent the lock's internal state, which we call Fields(, exc, rc, ), containing both the exc and rc fields, and also the stored value, .We can tie the first two fields to the physical, in-memory values with a proposition like the following: IsRwLock(rw, ,  ) ≜ ∃exc, rc,  .Fields(, exc, rc, ) * (rw.exc ↩→ exc) * (rw.rc ↩→ rc) * . . .
Next, we use resources to represent the intermediate states that occur during lock acquisition.For example, write-lock acquisition has a moment where we have set exc but not observed rc; likewise, read-lock acquisition has a temporary state where we have incremented rc but not observed exc.
We use ExcPending (𝛾) and ShPending (𝛾) to represent these states.So for example, to prove lock_exc, which has the intended specification, With shared access to rw.exc ↩→ exc and rw.rc ↩→ rc, we can use Guard-Atomic-Inv to perform the requisite atomic CAS and atomic load.However, because all these resources are shared, we cannot hold onto them for the duration spanning both operations at once.Therefore, when performing the CAS, the triple (exc, rc, ) used might be different than the triple used for the later load instruction.This is why we cannot track the intermediate "pending" state as part of the Fields resource, and need to use a separate ExcPending resource for the thread.With this sketch in place, we can observe some of the operations we need: for the CAS operation, we update exc from False to True and should somehow obtain ExcPending() in the process.So we need: Fields(, False, rc, ) ⇛ Fields(, True, rc, ) * ExcPending (𝛾) For the second step, reading the rc value, we find we need: This update requires us to observe that rc = 0, though it does not change rc or any of the other fields.It does, however, move us from the pending-exclusive state to the actual exclusive-lock state, while also acquiring exclusive ownership of the protected resource, as was our goal.
Figure 8 shows all of the operations that we need, including those we could determine from a similar analysis of the lock_shared implementation.This also includes an additional proposition, RwFamily(,  ), that ties  to the proposition family  .Now, we just need to use a storage protocol to construct the resources of Figure 8 and prove these the desired updates.Then we can complete the Hoare proofs based on the above plan.
Step 1: Constructing the ghost resources via a storage protocol.The first step in building a storage protocol is to determine the storage monoid and the protocol monoid.In our example, the storage monoid  can be Excl( ); i.e., there is either one thing stored, or there is not.
Our primary effort, then, is the protocol monoid .We define  to have a component for each class of proposition it needs to support.First up is the Fields proposition, and we know there should always be one such; therefore, we can represent it with Excl.Next, there should always be at most one of ExcPending or Exc, so we can use Excl for these as well.Meanwhile, there might be any number of ShPending propositions at a given time, so we can use N for these.
Finally, for the Sh propositions, we can have any number, but they need to agree on the value of .For this, we use a monoid AgN( ), which tracks a single value and a count.It is given by,

𝜖 | agn(𝑥, 𝑛) |
where  :  and  : N,  ≥ 1 with agn(, ) • agn(, ) = agn(,  + ) and for  ≠ , agn(, ) • agn(, ) = All in all, we can now declare our protocol monoid and name its important elements: , , 0, agn(, 1)) Now, we need to define S and C. First, S determines the element stored; this is given by  in the fields state, unless the lock is currently exclusively taken, in which case the storage is empty: Next, we define C to be False if any entry is or the first entry is ; otherwise, (where count(agn(, )) =  and count() = 0) These predicates can be stated in plain English: the reference count rc is the total number of threads with the shared lock or in the process of acquiring it; the exc field indicates whether any thread has the exclusive lock or is in the process of acquiring it; an exclusive lock cannot be taken at the same time as a shared lock; the value taken by a shared lock should match the fields's  value.
Now, with the storage protocol established, we can embed these elements as propositions: we let Fields(, exc, rc, ) ≜ ⟨fields(exc, rc, )⟩  and so on.We also let RwFamily(,  ) ≜ sto(,  ′ ), where  ′ (ex()) ≜  () and  ′ () ≜ True.Now, in order to show our desired reader-writer lock rules (Figure 8), it suffices to show the following updates (by SP-Update): Finally, we can prove all these just by expanding the definitions and using the logical invariants encoded in C.
Let us summarize exactly what the storage protocol gave us in this particular proof strategy.We wanted to construct some set of ghost resources with certain relationships, mostly updates (⇛), representing specific implementation details of the lock, with a single ⤔ proposition that enables sharing.The storage protocol shows how to reduce those desired relationships to proof obligations (⇝, ⇝, ⇝, ⇸) about monoids that can be expressed in first-order logic.These obligations all encode properties that should map cleanly to an intuitive property of the system, e.g., the ⇝ proposition intuitively means "from the intermediate pending state, if rc = 0, then the stored resource can be withdrawn," while sh() ⇸ ex() intuitively means "any reader agrees with the source-of-truth on what the shared value is."These properties all rely on our definition of S, a predicate that encodes which states of the system are well-formed.
Step 2: Verifying the implementation.To verify the implementation (Figure 6) against the spec (Figure 1), we first need to nail down a definition for IsRwLock(rw, ,  ).Since  is meant to be the unique identifier for the reader-writer lock, we can have it be the same as the ghost name  from the RwLock logic, and likewise  , the family of propositions protected in the lock, be the same as  , the family of propositions protected by the RwLock protocol.
The propositions representing the reader-writer lock should, as a whole, include the proposition RwFamily(,  ), the permission to access the rw.exc and rw.rc memory cells, and the Fields proposition which has the ghost data to match the contents of the memory cells.
IsRwLock(rw, ,  ) ≜ RwFamily(,  ) * ∃exc, rc,  .Fields(, exc, rc, ) * (rw.exc ↩→ exc) * (rw.rc ↩→ rc) The proof for rwlock_new is then straightforward: the implementation allocates the rw.exc and rw.rc memory, and we can ghostily instantiate the RwLock protocol via Rw-Init.Likewise, the proof for rwlock_free is straightforward, since its precondition requires that the caller has exclusive access to IsRwLock, so we can destructure it and use the exclusive ↩→ propositions in order to call free.
The proofs for the other methods, which must operate over a shared IsRwLock, are more interesting.Recall the proof outline for lock_exc: [RwFamily(,  ) * ∃exc, rc,  .Fields(, exc, rc, ) * (rw.exc ↩→ exc) * (rw.rc ↩→ rc)] The gist is that, in the first half, we apply Rw-Exc-Begin (in the case that the CAS succeeds) to obtain ExcPending (𝛾), and in the second half, we apply Rw-Exc-Acqire to complete the acquisition and obtain the desired state Exc() * ⊲  ().
Let us walk through the first half in detail.Since CAS is atomic, we can apply Guard-Atomic-Inv to "open" the shared proposition for the duration of the atomic operation.Thus, we need to show, If CAS succeeds, we have exc = False, so we apply Rw-Exc-Begin.This ensures we have ExcPending (𝛾) in the success = True case.Otherwise, we do nothing, and the program loops.The second half of lock_exc, where we atomically read rw.rc, is the same, using Rw-Exc-Acqire.

HASH TABLE EXAMPLE: COMPOSITION WITH SHARED STATE
In this section, we show how to use Leaf to verify a larger application built on top of the readerwriter lock developed in the previous section.In particular, while the reader-writer lock gives us a mechanism to obtain and release shared ghost state, here we will see how to make use of shared state.In particular, we show we can employ a fine-grained locking scheme, acquire multiple shared locks, and compose state to perform nontrivial operations.While the previous section was primarily an application of the storage protocol ( §3.4), this section will primarily be about applying the general-purpose Leaf rules and ghost state ( §3.1- §3.3).
The example we consider is a concurrent linear probing hash table [Knuth 1998], using a single lock per entry of the hash table.We choose this example primarily because a single operation requires us to take multiple locks, and in particular, for a query operation, we will be taking those locks in shared, read-only mode.Thus, this example tests Leaf's ability as a logic for manipulating shared resources.
Linear probing, here, is a particular strategy a hash table uses to handle hash collisions.Specifically, the hash table is arranged as an array with indices 0 ≤  < , with some hash function  : Key → [0, ).To insert a key-value pair (, ), we attempt to insert it into the array at position  =  ().If there is a different key already in the slot , then we attempt to insert into  + 1, and so on.Queries are similar: we scan starting at  until we find the key or an empty slot.
In our implementation, we let ht be a record {slots, locks} consisting of two arrays: one for the hash table slots, and one for the locks protecting them.We then implement query and update.For simplicity, we do not handle hash table resizing (i.e., we assume a fixed ).For the paper version only, the program aborts whenever a probe reaches the end of the table (index ).This keeps the presentation manageable, though our full Coq version does gracefully handle the last case.First, let us nail down the spec we want to prove.There are a handful of options; here, we choose one that manages the key-value mapping with propositions m( ht , , ), (analogous to ↩→ propositions).As with the reader-writer lock, we also have a main proposition IsHT(ht,  ht ) to say that ht is a hash table, and since the hash table is concurrent, the specifications for query and update require a shared IsHT(ht,  ht ).In order to define the IsHT and m propositions and prove the specifications, we once again start by defining a custom protocol of ghost state to represent the hash table's operation.
Step 1: Constructing the ghost resources.First, we create a custom ghost state that lets us relate the contents of a hash table's slots to the key-value pairs stored in the map, and which also encodes the appropriate kinds of state updates.Here, we just use "ordinary" PCM ghost state ( §3.1), as we do not need to create any new ⤔ relations.Specifically, we can take the monoid (Key ⇀ Excl(Value ?)) × (N ⇀ Excl((Key × Value) ?)) and set,

𝛾
For validity, V, we encode invariants of the hash table: the key-value pairs and slot entries are consistent, the slots obey hash-probing invariants, and so on.(See Appendix F for full details.)We can then derive: m(, , ) * slot(, , None) * ( * ()≤< slot(, , Some(  ,   ))) ⇛ m(, ,  ′ ) * slot(, , Some((,  ′ ))) * ( * ()≤< slot(, , Some(  ,   ))) Now, what happens when we consider that the slot state slot(, , ) might be shared and read-only (via the reader-writer lock)?Fortunately, the query-related rules can easily be applied even if the slot or m state is shared.In particular, each query-related deduction concludes a pure proposition, so we can apply Unguard-Pers to obtain the predicate.The update-related rules, meanwhile, can just be applied normally using exclusively owned state (though we could, in principle, apply Guard-Upd to UpdateExisting if we needed to, since its * term remains unchanged by the update).
Step 2: Verifying the implementation.Once again, we need to establish a definition for IsHT( ht , ht), the shareable proposition that makes something into a hash table.Our hash table here is just made up of  reader-writer locks: where we let   = .(ht.slots[] ↩→ ) * slot (𝛾, 𝑖, 𝑠).We also roll up all the relevant ghost names into the super-name  ht = (,  0 , ...) to serve as a name for the hash table as a whole.
The definition of   effectively says that the th lock protects both the permission to access the th slot of the hash table ht.slots [𝑖], but also the slot state in the ghost protocol.
Crucially, composing the IsRwLock propositions with * makes IsHT easy to work with whether or not it is owned exclusively or shared.In particular, if we have IsHT( ht , ht) shared, as we expect of a concurrent hash table, then we can use Guard-Split to obtain a shared IsRwLock(ht.locks[])so we can perform the usual lock operations (lock_shared, and so on).
For the proof, we focus on query here, as it makes the more interesting use of shared state.The meat of query is in the recursive query_iter.Our proof of query_iter is inductive, and it takes in its 9. Proof outline of query_iter precondition the ghost state for all the slots previously accessed in the probe, in addition to the preconditions of query.Figure 9 shows a proof outline.
Stepping through, we first call lock_shared; using our shared IsRwLock(ht.locks[],  ,   ).From this call, we obtain Sh(  , ), for some  which is fixed for the duration we hold the lock.By Rw-Shared-Guard and the definition of   , we have Sh(, ) ⤔  (ht.slots[] ↩→ ) * slot(, ) (eliminating the ⊲ by Later-Guard).In the outline, we represent this shared state with our [. ..] notation.Now, by Heap-Read-Shared we can perform the !ht.slots[] operation to load the value of  and case on it.If the slot is empty (None) then we apply QueryNotFound to get our answer; if the slot is full and the key matches, then we apply QueryFound.As discussed above, we have all we need to apply these deductions even when the state on the left-hand side is shared.The most interesting case is the recursive one: here, we append the newly obtained slot(, ) to obtain * ()≤≤ slot(, , Some(  ,   )), meeting the precondition for the recursive call.
The Client of the Hash Table .There are many options available to the hash table's client.We presume that the client wishes to share the hash table between threads, and she has the freedom to do this as she wishes.For instance, she might share it between a fixed number ( ) of threads, using a fractional paradigm to give out a fraction 1/ to each (Example 3.2).Alternatively, she could allocate it permanently and share it forever (Example 3.4).Or she could put the hash table inside yet another reader-writer lock, with multiple threads able to concurrently access the hash table by taking a shared lock.This last possibility could be a step to augment our design with resizing: a client could take the lock exclusively to "stop the world" and rebuild the hash table.
Shared State with the Hash Table .One might wonder if the client could further apply Leaf and use the hash table to store propositions and manage shared access to them.For example, we might want to say m( ht , , ) ⤔  (, ) for some proposition family  ; then any client with shared access to the key  would also get shared access to the resource  (, ).Indeed, we can modify our Hash Table Resource to allow this.Specifically, we could reconstruct the resource via a storage protocol so we can prove ⤔ propositions.Effectively, the existing hash table monoid construction would become the protocol monoid for this new storage protocol.

MORE ADVANCED STORAGE PROTOCOLS
The lock example in our paper is intentionally kept somewhat simple for the sake of exposition.However, subsequent work has already used Leaf's storage protocols to verify far more sophisticated read-sharing mechanisms.Specifically, IronSync [Hance et al. 2023] is a verification framework that combines storage protocols with a handful of other techniques (notably, using a substructural type system to manipulate ghost resources, including shared ghost resources, rather than using CSL directly).Their framework embeds Leaf's monoidal storage protocol definitions as axioms for manipulating their ghost resources.
They describe their experience using storage protocols to verify: • A multi-counter reader-writer lock with additional, domain-specific features.This is a component of an effort to verify a multi-threaded page cache; the lock is used to protect a 4 KiB cache page, and the domain-specific features relate to reading and writing the cache page from disk.The lock not only allows read-sharing of memory resources for the 4 KiB pages, but also of ghost resources related to their contents.• A concurrent ring buffer with multiple producers and multiple consumers, where entries are alternately writeable and read-shared, as producer threads enqueue messages to be read (possibly simultaneously) by a number of consumer threads.This is a component of a state replication algorithm [Calciu et al. 2017], targeting non-uniform memory access (NUMA) architecture.Once again, this not only allows read-sharing of memory resources, but also ghost resources related to the operation log.The second example, in particular, demonstrates that read-sharing protocols extend beyond readerwriter locks.Furthermore, both examples (similar to our hash table) demonstrate the use of readshared custom ghost resources.

SOUNDNESS
Here, we sketch our construction of the Leaf logic within the Iris separation logic; for full details, consult the Coq development.To get the most out of this section, it helps for the reader to be already familiar with Iris; Jung et al. [2018] provide all necessary background.
As context, we review the components of Iris.First, there is the Iris base logic, a step-indexed logic of resources in the abstract, with no primitive notion of a program or Hoare logic.The Iris base logic is proved sound via a semantic model called the UPred model.Then, atop the base logic, Iris can do a variety of useful things, e.g., instantiate a program logic given some operational semantics.
To build Leaf, we add a few minor deduction rules to the base logic, proved sound via the UPred model.Our additions to the base logic are given in blue.We then define ⤔, ⟨⟩  , and sto(,  ), and prove all of Leaf's deduction rules within the Iris logic.The rest of the Iris framework, such as the machinery to instantiate a program logic and prove adequacy theorems, is unchanged.
Ghost state.In Iris, ghost state is constructed from a mathematical object called a CMRA.A PCM is a special case of a discrete CMRA, and the ghost state (   ) in this paper is just the usual Iris ghost state.We also add the PCM-And rule to the base logic, which follows straightforwardly from the definition of ∧ over the UPred model and holds for any discrete CMRA.
Invariants.Our definitions build on Iris's invariants, so we review those here.Iris defines (within the base logic) a persistent proposition   as knowledge that an invariant  is allocated at name .
Iris then proves the following rules so the user can allocate, open, and close invariants: The definition of ⤔.To define ⤔, we first define a "pre-guards" operator, ⤔, for finite sets  , and then take the closure under supersets to define the real ⤔.The ⤔ definition can be read roughly as, if we have the invariants at  and some other state , which can separate into  and some other component, then it also can separate into .Note that this definition does not perform an update ( | ⇛) or even a mask change.If we used | ⇛ we would not be able to show Guard-And because it would require us to perform two potentially contradictory updates simultaneously.It is also important that the definition preserves OwnInvs( ), so that we can prove Guard-Trans without needing an additional disjointness condition.Finally, taking the closure under supersets lets us prove Guard-Weaken-Mask.
Basic ⤔ proofs.From this definition of ⤔, many Leaf rules are straightforward: Guard-Refl, Guard-Trans, Guard-Split, Guard-Weaken-Mask, Guard-Pers, and Unguard-Pers.To prove Guard-Upd, we perform a view shift from E 1 ∪ E 2 to E 1 , opening all the invariants (Inv-Open) in some  ⊆ E 2 .This is primarily where we use the finiteness of  and the AllocatedInvs( ) term.Later-Guard follows thanks to our use of ⋄ in the definition of ⤔.Later-Pers-Guard then follows by pulling out the persistent part with Unguard-Pers, and then applying Later-Guard.
Point Propositions.Finally, we come to Guard-Implies and Guard-And.The inherent difficulty here is that  ⊢  does not in general imply  can separate into  * ( − * ).We therefore need to use the fact that  is a point proposition.The key is that point propositions are all of the form Own () where  is an element of the global CMRA instantiating the Iris model.We can show: This rule is somewhat peculiar: initially it looks like it must be wrong, since applying  and  − *  ought to give you just , not  plus something else.On second glance, it makes some intuitive sense that you ought to be able to "go back" to , since you had  to begin with.At any rate, it does at least hold for the Own () propositions, and it follows from the model definitions of * , − * , and Own ().Reynolds [2008] proved a similar theorem about what they called strictly exact assertions; strictly exact assertions are not representable in Iris, but Own () propositions play a similar role.
To finish the proofs, we need something to account for our use of ⋄ in the definition of ⤔.From this, the proofs of Guard-Implies and Guard-And follow.

𝛾
In other words, the sto predicate gives us the invariant at location , which contains the authoritative copy of some initialized protocol along with any state currently stored in the protocol.
We can now proceed with the proofs of the laws in Figure 5b.To prove SP-Exchange, we use our knowledge of the invariant from sto (𝛾, 𝐹 ) to open it, obtaining the stored state  (S()) and the authoritative knowledge of the protocol state, • prot()

𝛾
. We perform the update and then close the invariant.SP-Deposit, SP-Withdraw, and SP-Update are just special cases of SP-Exchange.
Mechanization.The above definitions and proofs are formalized in Coq, on top of the Iris library.Our Coq formalization also includes the instantiation of Leaf on a heap-based language with atomic reads and writes ( §3.3), and the proof of the lock-based hash table ( §4 and §5).

RELATED WORK AND COMPARISONS
Shared, read-only ownership in separation logic.Fractional permissions and counting permissions are existing mechanisms for temporarily shared read-only state that have appeared in a variety of contexts.It is well-established that one can represent these via monoidal ghost state, but to our knowledge, the existing approaches do not provide a uniform way to reason about their interpretations as read-only state the way Leaf's ⤔ operator does.Below, we examine in depth what our case study would look like if we used traditional fractional resources.
Fractions have also been used in Iris's cancellable invariants, i.e., invariants which are temporarily shared and then reclaimed, just as Leaf's guarded propositions are.However, Iris's cancellable invariants do not support overlapping conjunction the way Leaf's ⤔ does.
Fractions have also been used for a variety of more sophisticated applications, such as RustBelt's lifetime logic [Jung et al. 2017] which can reason about Rust's shared borrows.Again using fractions, Dang et al. [2020] show how to handle shared resource reclamation in relaxed memory settings.We leave it as future work to see if Leaf's more general protocols can be adapted to those domains.Charguéraud and Pottier [2017] introduce a "temporary read-only modality" for the sequential setting, allowing a user to temporarily exchange a resource for a read-only resource, managed by a lexical scope rule.In Leaf, shared resources are not necessarily bound to a fixed scope: guard propositions can be conditional and their scope dynamically determined.
Some prior work has also demonstrated more general permission logics capturing fractional and counting use cases [Parkinson 2005], some with arbitrary composition structure [Dockins et al. 2009].However, these methods require all state from the permission logic to be collected in order to regain exclusive access.To our knowledge, they are not flexible enough to support protocols like our RwLock protocol, which must represent nontrivial states even when no proposition is stored.Meanwhile, fictional separation logic [Jensen and Birkedal 2012] uses a general extension mechanism similar to Leaf's relationship between protocol and storage monoids, though it does not address general read-sharing mechanisms.
Concurrent separation logics with custom ghost state.Many CSL frameworks have introduced custom ghost state, i.e., mechanisms for users to define new resources, such as the work on Concurrent Abstract Predicates (CAP) [Dinsdale-Young et al. 2010;Svendsen and Birkedal 2014], Fine-grained Concurrent Separation Logic (FCSL) [Nanevski et al. 2014] with its concurroids (also based on PCMs), and Iris with its CMRAs (see below).To our knowledge, none of these frameworks have yet been used to provide a modular representation of temporarily-shared custom resources that supports the composition of resources shared via the representation.
Iris's ghost state formalism.Leaf creates a custom ghost state mechanism, storage protocols, based on PCM ghost state.Iris has generalized PCM ghost state in a different direction, creating an algebraic object called a CMRA [Jung et al. 2016], which has two new features over PCMs.The first is a built-in notion of persistent state; in Leaf, we de-emphasize persistent state because of our focus on temporarily-shared state.However, it would be straightforward to incorporate persistence into the protocol monoid formalism.The second is a step-indexed notion of equality, which makes CMRAs suitable for higher-order ghost state and many of the foundational elements of Iris.In Leaf, we wanted our storage protocols to be easily represented in first-order logic with a discrete notion of equality.Factoring our map into two steps, S :  →  and  :  → iProp is what allows us to define a storage protocol without any step-indexing.As such, one can understand storage protocols as a particular ghost state abstraction built on CMRA machinery.
Verified hash tables.Hash tables have been verified before [Ho and Protzenko 2022;Polikarpova et al. 2017;Pottier 2017], including a concurrent one done in Iris that uses mutual exclusion locks [Clausen 2017].Our concurrent hash table has some crucial differences that make it interesting: (i) we use reader-writer locks, and thus shared ownership for queries, and (ii) ours requires a single operation (update or query) to take more than one lock.
Case study comparison.It is worth comparing explicitly to how our reader-writer lock and hash table case study might be done if we used more traditional techniques.One of the most common such techniques in use to day is-as we have referenced several times in this paper-the technique of fractional permissions.If we were to build the hash table case study using fractional permissions (in Iris, or in any other framework supporting monoid ghost state and invariants), it might look something like the following: • First, the resource being protected by the lock would need to have a built-in notion of being fractional.The reader-writer lock spec could be parameterized over a fractional proposition family,  (, ).The IsRwLock(rw, ,  ) proposition would need to be fractionalized as well, which could be done using a technique called cancellable invariants (invariants with associated fractional tokens, which allow the inner resources to be reclaimed).Ultimately, the lock's Hoare triples would look something like (to select a few): while the client would have to promise that  (,  0 ) *  (,  1 ) ⊣⊢  (,  0 +  1 ).Also observe that Sh to track the fractional amounts that are "lent out," so we can make sure the same amount is returned later.• To verify the reader-writer lock, we would internally define an invariant that maintains some possibly-fractional amount of the resource, so that it has something to "lend out" whenever a client takes a read lock.Further, we would need to create ghost resources to define Sh(, , ), Exc(), intermediate states, and so on.These resources would need to keep track of all the fractional amounts that are "lent out, " and make sure they sum to the correct amount.We would still need to reason about the intermediate states of the locking operations, but now the relationships are slightly harder to specify, because they interact with all of the fractional accounting.• The client of the lock (the hash table) would need to make sure the resources it uses have a built-in fractional notion so they can interoperate with the lock.Thus the points-to operations would need a built-in notion of fractions (ℓ frac ↩− − →  ) while the hash table's "slot resources" slot(, , ) would be replaced by fractional resources slot(, , , ).Now, all the updates and deductions would be expressed with fractions, and proving the ⇝ relations would involve reasoning about a composition operator • that adds rational numbers.For example, one has to reason like, "Suppose I have a  fraction of slot , and a unit amount of slot  + 1, and I replace slot  + 1 with a unit amount of a new slot value... " While this is all certainly possible, our perspective is that it involves a large number of "bureaucratic" details that do not directly relate to the programmer's primary intuition of why the program is correct.By contrast, when doing this in the Leaf style, as we have seen: • The RwLock specification-that is, the "interface" between the two components-becomes cleaner.Neither IsRwLock, Sh, nor  need an additional rational number parameter (Figure 1).Instead, the relationships between these components and the sharing that takes place are all made clear through the ⤔ and [. ..] {. ..}  {. ..} notation.• The RwLock is easier to verify because the storage protocol formulation helps us reduce the problem to a series of proof obligations regarding the evolution of the system.• The Hash Table is easier to verify in Leaf because we can reason in a manner similar to how we would do it in an exclusive ownership setting, without the encoding having to "bake in" sharing-related details.For example, we would reason something like, "Suppose I have slot  and slot  + 1, and I replace slot  + 1..." and then rely on Leaf to apply this in the presence of shared slots.This sort of simplification would apply to any application that uses fine-grained reader-writer locks in a similar manner.

CONCLUSION
We have introduced Leaf, a concurrent separation logic with an approach to temporarily shared ownership based on our novel guarding operator, ⤔.We showed that Leaf can help the user implement and verify sharing strategies, that it allows modular specifications involving shared state that abstract away the sharing mechanism being used, and that Leaf's composition capabilities allow it to handle fine-grained concurrency.

B LEAF DEDUCTION RULES B.1 Iris Background: PCM Custom Ghost State
Iris provides ghost state via an algebraic object called a CMRA (sometimes camera).A PCM is a special case of a CMRA, and in particular, a PCM is a special case of a discrete CMRA (i.e., a CMRA whose equality does not depend on the step-index).The version we present here is a simplified picture of Iris ghost state based on PCMs.Note that PCM-And relies on the discreteness so that ⪯ is well-defined.Formally, a PCM is a set  (the carrier set) with a composition operator • :  ×  → , and unit  : , and validity predicate V :  → Bool, where we let,

B.4 Remarks and Counterexamples
We provide counterexamples to rules that one might initially expect, or hope, to be true.

B.4.1
The restriction on Guard-Implies.Suppose this rule held: We will derive a contradiction.Take any propositions ,  such that We will derive a contradiction.Take fractional memory permissions.We have (ℓ frac ↩− − → 1/2 ) ⤔ Frac (ℓ ↩→ ).We also have, We can apply Wrong-Guard-Upd twice: A special case would be, In fact, not even the special case is sound.To see why requires us to put an element of a storage protocol inside itself.At a very high level: take any timeless proposition  such that  *  ⊢ False.
Insert  into a storage protocol and obtain some  such that  ⤔ E .Then insert  into the same protocol and obtain  such that  ⤔ E ( * ).Now, if the above rule held, we could get ( * ) ⤔ E ( * ).Then by Guard-Refl we could get  ⤔ E  * , which should be impossible.
Let us demonstrate in a little more detail that this argument really is possible.It is not hard to construct a protocol, for any  : Set and  :  → iProp, such that, upon initialization, we have the rules, where (, ) are duplicable propositions.Let  = { } ∪ Name for some special value .Note that this is enough to define the storage protocol.We can now deposit  () =  to get  (𝛾, 𝜔).Call this new proposition .We then have  ⤔ { E } .
• We can prove UpdateExisting and UpdateInsert by PCM-Update.
• We can prove the last three rules by PCM-And.
→   (where ℓ : Loc,  : Value,  : Q, and 2.4.2Nontrivial Guarding with Storage Protocols.Now, we return to our question from earlier: how can we, in general, soundly construct propositions like (ℓ frac ↩− − →  ), (ℓ ro ↩− → ), or Sh(, )?What are the primitive deduction rules for ⤔, and in particular, what are the rules that allow us to prove nontrivial ⤔ relations on those propositions?

Fig. 3 .
Fig. 3. Deduction rules for PCM-based ghost state and view shifts.
Fig.6.Implementation of a reader-writer lock.We assume all heap operations are atomic, including CAS (compare-and-swap) and FetchAdd.

RwLock
Fig.8.A custom resource derived via the storage protocol mechanism, designed for our particular implementation.