An Iris Instance for Verifying CompCert C Programs

Iris is a generic separation logic framework that has been instantiated to reason about a wide range of programming languages and language features. Most Iris instances are defined on simple core calculi, but by connecting Iris to new or existing formal semantics for practical languages, we can also use it to reason about real programs. In this paper we develop an Iris instance based on CompCert, the verified C compiler, allowing us to prove correctness of C programs under the same semantics we use to compile and run them. We take inspiration from the Verified Software Toolchain (VST), a prior separation logic for CompCert C, and reimplement the program logic of VST in Iris. Unlike most Iris instances, this involves both a new model of resources for CompCert memories, and a new definition of weakest preconditions/Hoare triples, as the Iris defaults for both of these cannot be applied to CompCert as is. Ultimately, we obtain a complete program logic for CompCert C within Iris, and we reconstruct enough of VST's top-level automation to prove correctness of simple C programs.


INTRODUCTION
Iris [Jung et al. 2015], a language-independent framework for concurrent separation logic (CSL) proofs in Coq [Coq Development Team 2023], has been the platform for a wide range of recent research projects: on distributed systems [Krogh-Jespersen et al. 2020], weak memory [Dang et al. 2019;Kaiser et al. 2017], crash safety [Chajed et al. 2019], and many more.Most instantiations of Iris use simple core calculi capturing the key features of interest (shared memory, message passing, weak-memory concurrency, etc.), but several model code in (fragments of) real languages, including Go [Chajed et al. 2019], Rust [Jung et al. 2017], C [Sammler et al. 2021], and WebAssembly [Rao et al. 2023].Most of these instances use ad-hoc language semantics developed for Iris, and in many cases their code cannot even be run-they are relational models of the languages, not executable semantics or interpreters.A notable exception is Iris-Wasm [Rao et al. 2023], which is built on an existing formal semantics derived from the WebAssembly reference interpreter [Watt et al. 2021].In this paper, we instantiate Iris with the C semantics of CompCert [Leroy 2009], a verified compiler for a large subset of C.This allows us to use Iris to verify real-world C programs that can then be compiled with CompCert and executed, using the same semantics for the separation logic and the verified compilation.This is probably the largest instantiation of Iris with a pre-existing semantics, and certainly the first attached to a verified compiler.
The paper is structured as follows.In section 3, we present the model of our logic, and define the basic points-to predicate.In section 4, we define safety via a weakest precondition assertion, and prove the adequacy theorem that relates the logic to CompCert's semantics.In section 5, we re-prove the Hoare rules of VST on top of our logic, giving us a working program logic for C. In section 6, we apply our logic to some of VST's example programs.In section 7.1, we discuss the structure of our Coq development and compare it to the corresponding components of VST.In sections 8 and 9, we review related work and discuss potential applications and extensions.
A note on concurrency.Separation logics in Iris are naturally concurrent, but CompCert's semantics are strictly sequential.Thus, while the program logic we present in this work could easily be adapted to concurrency, our adequacy result only applies to sequential programs, and our logic should be considered a sequential logic.We discuss in section 9.1 some possible pathways to a concurrent separation logic for (a concurrent extension of) CompCert C.

BACKGROUND 2.1 Iris
Iris [Jung et al. 2018] is a language-independent separation logic framework implemented in the Coq theorem prover [Coq Development Team 2023].Its key innovation is a very generic formulation of ghost state, abstract logical state that is used in proofs to model features of the program under consideration that are not explicit in the semantics, such as protocols for access to shared resources.Iris defines a form of step-indexed resource algebra, called a camera, that behaves "enough like a heap" to be targeted by separation logic assertions, and then defines a generic separation logic on an arbitrary collection of cameras.Crucially, because step-indexing is built into the definition of cameras, they can be used as higher-order ghost state: they can contain program logic assertions, Hoare triples, state machines where each state is associated with an assertion, and a wide range of other useful constructions.
where ⪯ ≜ ∃ .= • A camera consists of a carrier set , a validity predicate ✓, a core operation | • |, and an operator • that together satisfy the rules shown in Figure 1.The key feature is the associative, commutative operator •, which is used to implement separating conjunction of resources.The core | | of a resource represents persistent information that can be arbitrarily duplicated once learned: for instance, in an algebra of transition systems, | | represents the knowledge that the system is in a state reachable from state .Finally, in separation logic proofs, we are only allowed to own valid elements of the camera: this allows us to rule out the coexistence of particular elements, e.g., to declare that two threads cannot both have full ownership of the same memory location.
Cameras are injected into the separation logic via the ghost state ownership assertion , where is an element of a camera and is a ghost name where the element is held (analogous to a memory location in a physical heap).Some of the rules of ghost state ownership are shown in Figure 2. We can allocate new ghost state containing any valid element of a camera at any point in a program.We can also change the value of ghost state using a frame-preserving update, written as ⇝: we can change to as long as any frame consistent with is also consistent with , i.e., the change * ⊣⊢ Fig. 2. Ghost state ownership from to does not invalidate any possible frame.The separation logic operator | ⇛ means "true under a frame-preserving update", and is incorporated into program logics through an extended rule of consequence: { } { } where ⇛ is shorthand for ⊢ | ⇛ .In other words, we can freely perform frame-preserving updates to ghost state ("view shifts") at any point in a verification.
As an example of ghost state, consider the common proof pattern in which one party holds authoritative knowledge of the current state of a resource, and another party holds partial knowledge of one aspect of the state.For any camera , we can define the authoritative camera on as containing elements of type ?× ?, with validity defined such that ✓ ( , ) ⇔ ⪯ .Then • ≜ ( , ⊥) can be used to represent the authoritative value of a resource, and • ≜ (⊥, ) can represent a fragmentary or partial value, with a guarantee that if one party knows • and another party knows • , then ⪯ .This algebra allows frame-preserving updates that simultaneously modify the authoritative and fragmentary parts of the resource: for instance, if is the algebra of key-value maps and ∉ dom( ), then allowing us to use a view shift to add a new key to the authoritative map and a client's knowledge of the map simultaneously.
Another common use of ghost state, and specifically higher-order ghost state, is to implement separation-logic invariants that are true at every step of a program's execution.The construction of invariants is somewhat complicated (see Jung et al. [2018]), but it rests on the agreement algebra, defined such that ✓ (agree( ) • agree( )) ⇔ = .By instantiating this algebra with separation logic assertions, we can define an assertion that stores an assertion for access by any party at any time, as long as the accessing party restores by the end of the operation.The name is used to identify the invariant and ensure that it is not opened while already open; Hoare triples in Iris are indexed by a set E of enabled invariants, and invariants are accessed through a mask-changing view shift is effectively equivalent to * ⇛ (i.e., the view shift ⇛ E E\{ } allows us to open the invariant ).This kind of view shift is similarly reflected in an extended rule of consequence, where all invariants can be accessed and then restored between program steps, and is also used in the specification of atomic program operations, which can access invariants during their execution and restore them afterwards.

The Verified So ware Toolchain
The Verified Software Toolchain (VST) [Appel et al. 2014] is, like Iris, a Coq-based separation logic verification system.It has its own generic framework for separation logic, but the vast majority of its development is specialized to one particular programming language: Clight, the first intermediate language of the CompCert verified compiler [Leroy 2009].Clight is a syntactically restricted subset of C supporting most core language features, and CompCert's correctness proof guarantees that the assembly code generated from a Clight program has the same behavior as the source program.VST defines a program logic for Clight, called Verifiable C, as well as an automation library for Verifiable C, called Floyd [Cao et al. 2018].The system has been used to prove correctness of a range of real C programs in Coq, including cryptographic functions [Beringer et al. 2015], a concurrent message-passing system [Mansky et al. 2017], and a web server [Zhang et al. 2021].Unsurprisingly, reasoning formally about real C programs is more difficult than reasoning about core calculi, and VST provides rules and automation for features like integer bounds, pointer validity, struct representation, and other details that are not present in most separation logic systems.At the same time, VST is a much older system than Iris, and while recent work has updated it with some Iris features, including limited higher-order ghost state and the Iris Proof Mode [Mansky 2022], it is still much less flexible and extensible than Iris.
In terms of the layout of VST shown in figure 3, in this work we replace VST's logical framework (MSL) with that of Iris, and then define a new resource model and definition of Hoare triples for Clight in the Iris style.We then state and prove all the Hoare rules of Verifiable C, VST's program logic, in our Iris-based logic, obtaining an Iris instance for CompCert C with the same surface rules as VST.Our new logic can be used to verify any C program previously verified with VST, but can also take full advantage of Iris's higher-order ghost state, proof mode, and other theoretical and practical innovations.Our resource model and our proofs of the rules of Verifiable C are inspired by VST, but are realized quite differently, building on ideas from Iris and taking advantage of its higher-order capabilities to do almost all our reasoning within the separation logic, rather than unrolling the assertions into propositions over program states.We have also mostly reconstructed VST's automation (Floyd) and used it to re-verify some simple example programs (see section 6), but due to the nature of tactic programming in Coq there are still many bugs that will only be observed in sufficiently complicated examples, and we do not present a complete automation system as part of this work.

A RESOURCE ALGEBRA FOR COMPCERT MEMORY
As described in section 2.1, an Iris assertion is a predicate on a collection of resource algebras, called cameras, that define the resources in the logic.Unlike other separation logic systems, Iris makes no hard distinction between "real" and "ghost" resources: all resource assertions involve ownership of some element of a camera, and it is only in the definition of Hoare triples that one particular camera is connected to the actual physical state of the program via a state interpretation assertion .By default, the physical state is a map from locations to values, and points-to assertions are defined via ownership of fragments of that map1 : By the rules of the authoritative camera, • * •[ℓ := (_, )] ⊢ (ℓ) = , so the points-to assertions reflect the physical values held in the heap.The fraction ∈ (0, 1] is used to share ownership of the location (e.g., between threads): any fraction of ℓ is sufficient to load from ℓ, while full ownership is required to store to it.
CompCert complicates this picture in several ways.First, some memory locations should never be targeted by ordinary load and store operations.Any realistic model of C must include function pointers, and VST also allows designating certain locations as locks for concurrency purposes.A CompCert memory will still mark these locations as allocated, and they may even hold values, but logically they do not contain those values: instead, we associate them with special resources, LK for locks and FUN for function pointers, that do not appear in the physical memory.Second, in the standard Iris model the physical state does not include any ownership information: the fraction is purely ghost state.However, CompCert memories include permissions at each location, chosen from Nonempty, Readable, Writable, and Freeable.The operational semantics of CompCert checks these permissions on each memory operation: a program cannot write to a location unless the location is Writable, and cannot free it unless it is Freeable.This helps ensure that optimizations verified in CompCert will continue to be sound under concurrency.There is a natural correspondence between ownership shares in separation logic and the permissions in CompCert memories.However, this correspondence points to another complication: ordinary fractional permissions do not distinguish between Writable and Freeable, and do not allow non-Readable but Nonempty ownership of a location.We must complicate our model of resource ownership to allow programs to hold enough of a location to know that it is allocated, but not enough to observe the location's value, so that other parties can write to that location but not free it.

The Camera of Shared Values
The natural algebra for representing partial ownership of resources is a pair ( , ) of a share and resource .By using the agreement camera for , where ✓ ( 1 • 2 ) ⇔ 1 = 2 , we can guarantee that any two parties that own part of a resource agree on the current value of that resource.We can then define and obtain exactly the combining and splitting rules we expect for heap resources.If we use fractions as our shares, with 1 • 2 ≜ 1 + 2 and ✓ ( ) ≜ ∈ (0, 1], then we have that all the shares of an element total to (at most) 1, and ownership of 1 guarantees full and exclusive control over the resource.Iris uses a slightly more complicated algebra-the shares also include a discarded share □, which represents read-only resources that can be freely duplicated and never changed-but essentially, this algebra is all we need to describe ownership of heap fragments and build a separation logic.
In this setup, the absence of 0 shares is essential.For instance, if thread were to hold a resource (0, ) while thread held (1, ), then thread would not be able to change the value of the resource to (1, ′ ): this would be inconsistent with thread 's knowledge of the value .In other words, changing the value of the resource is justified by holding a large enough share that no other thread can know the current value.CompCert, however, recognizes another possible distinction: it is possible for a thread to know that a location exists without knowing its current value, allowing other threads to write to the location without holding full ownership of it.In other words, fractional shares induce a simple lattice of ownership in which a thread may be able to read or write to a resource, with Readable < Writable; CompCert, on the other hand, uses a four-element lattice of Nonempty < Readable < Writable < Freeable.A thread with Writable but not Freeable ownership of a location can change the value of the location but cannot deallocate it; a thread with Nonempty but not Readable permission cannot read the value of the location, but can perform other operations (e.g., pointer comparison) that in C are only valid on allocated pointers.We can generalize our notion of shares to tree shares [Dockins et al. 2009], which naturally express this four-way distinction, but we will also have to modify the rules of our algebra to reflect the presence of unreadable shares that can never know the value of the resource.
The early days of concurrent separation logic saw a proliferation of mathematical models of shares/permissions for coordinating ownership of memory locations between threads.While fractions are a natural model, they are also a poor fit for some applications, especially the "token factory" pattern where a single main share splits off and recollects any number of interchangeable smaller pieces over time, while tracking the number of shares that have not yet been returned.Dockins et al. introduced tree shares [Dockins et al. 2009] as a unifying model for both fractional and token-factory shares: a share is a set of nodes drawn from an infinite binary tree, where each node can be split into its two child nodes.As applied in VST, the left subtree can be thought of as the "malloc-free share", and the right subtree can be thought of as the "read-write share".A thread that owns any nodes in the right subtree for a location ℓ can read ℓ's value, and a thread that owns the full right subtree can write to ℓ; a thread that owns any nodes in the left subtree can know that ℓ is allocated (but may not be able to read ℓ's value), and only a thread with full ownership of both subtrees can free ℓ.For our purposes, the details of tree shares are less important than the lattice they induce, in which a share may be empty; nonempty but unreadable; readable but not writable; writable but not full; or full; and any share can be split into two subshares.In fact, all the definitions in this section are agnostic in the underlying share implementation, requiring only that it satisfies this lattice; we specialize to tree shares in the next subsection.
The pair camera at the start of this section was convenient because we could combine shares and resources independently.In an algebra with unreadable shares, this is no longer the case: instead, our elements are more like dependent pairs { & (if readable( ) then else unit)}, where the second element may only be present if the first element is a readable share.Following VST, we write readable elements as YES( , ) and unreadable elements as NO( ), and define the algebra of shared values as follows: where ⊥ is an invalid "error" element (all non-⊥ elements are valid).Note that the combination of two unreadable shares is always unreadable; the combination of two readable shares, or one readable and one unreadable, may be unreadable, but only if the shares fail to combine (e.g., they add to more than 1 in the fractional case, or overlap in the tree-share case).
The algebra of shared values satisfies all the laws of cameras in Iris.It also admits the updates we desire: Lemma 3.1.If is a writable share, then YES( , ) ⇝ YES( , ′ ).
Proof.We must prove that YES( , ) ⇝ YES( , ′ ) is a frame-preserving update, i.e., for any such that ✓ (YES( , ) • ), it is also the case that ✓ (YES( , ′ ) • ).Let 2 be the share of .If readable( • 2 ) holds, then 2 must not be readable (since the combination of a writable and a readable share is ⊥), so must be NO( 2 ).Then YES( , , and both are equally valid.On the other hand, if readable( • 2 ) does not hold, then whether is YES or NO we have YES( , ) • = ⊥, which contradicts the assumption that ✓ (YES( , ) • ).□ In the pair algebra we started with, only the full share 1 was sufficient to change the value of ; in the algebra of shared values, we have shown that any writable share is sufficient.Now we can model the logical heap as a map from keys to shared values, which we call a resource map or rmap.Our definition of points-to is similar to that of Iris, but we also have an empty points-to assertion ℓ ↦ → _ to represent ownership of an unreadable share of a location: Ownership of both readable and unreadable points-to assertions can be split and combined according to their shares, and We also now have shares in the heap itself, so that the information we gain from points-to assertions is as follows: where share(YES( , )) = share(NO( )) = .Holding ℓ ↦ → guarantees that the value in the heap at ℓ is , and also that the share in the heap is at least ; holding ℓ ↦ → _ guarantees that the share in the heap at ℓ is at least , but does not tell us what the value is or even whether a value exists ( (ℓ) could be either YES or NO).This more complicated heap model both allows us to represent unreadable ownership of resources, and includes ownership shares in the authoritative heap.
The camera of shared values and its induced generalization of Iris's heap model are totally independent of the details of C, CompCert, or tree shares; they could be reused in any setting where we want to talk about non-readable ownership of a resource.The obvious application is to other malloc-free languages, where "writable but not freeable" is a natural permission level, but it could also be used in, e.g., a message-passing language where a thread can have enough permissions to know that a channel exists without being able to send or receive messages on it.

Coherence between Logical Heaps and CompCert Memory
Thus far, we have defined a generic algebra of heaps (rmaps) mapping locations to share-annotated resources.We will now define the type of resources we use for C, and relate our heap model to the memory model used in CompCert's semantics.
A CompCert memory [Leroy et al. 2012] consists of a set of numbered blocks, each of which contains a range of integer offsets.An address is a pair ( , ) of block and offset.For each allocated address, the memory tracks its current value (usually either a byte or Undef) as well as both current and maximum permissions (chosen from Nonempty, Readable, Writable, Freeable as described above).The memory also records the next block to be allocated, and guarantees that unallocated blocks have no values and no permissions.The separation of current and maximum permissions is useful for, e.g., distinguishing between read-only global variables and locations whose ownership is (temporarily) being shared.
In most Iris instances, the logical heap is the actual physical memory used by the operational semantics of the language.In our setting, however, a logical rmap does not uniquely determine a CompCert memory or vice versa.An rmap does not distinguish between an unallocated block and a block on which it holds no ownership, nor does it record the maximum possible ownership for a location.In the other direction, each CompCert permission except Freeable corresponds to an infinite number of possible shares: e.g., a Readable share can always be split into two smaller Readable shares.There are also two types of values in memory, locks and function pointers, that should never be the target of CompCert memory operations (load, store, etc.), and it is useful to track these separately in our rmaps.We reconcile the two by defining a coherence relation between rmaps and CompCert memories, and when we state the adequacy of our logic we will reason about all memories coherent with a given rmap.First, we define the relationship between rmap shares and memory permissions: While the logical share is used to determine the permission for value resources, non-value resources are associated with minimal Nonempty permissions regardless of their logical ownership.This is essential for relating assertions to CompCert memories: CompCert always assigns Nonempty permissions to function-pointer locations, ensuring that they are not targeted by ordinary memory operations, but we still want to be able to reason about the contents of those locations in the logic.We describe the function-pointer assertion in section 3.3.
Next, we define the correspondence between rmaps and memories.
Definition 3.2.An rmap is coherent with a memory when the following conditions hold: (1) If (ℓ) holds a value (i.e., not a lock or function pointer), then the value at (ℓ) is .
(3) Blocks that have not been allocated in are not in the domain of .
We could require that the current permission of ℓ be exactly perm_of ( (ℓ)), but this would not be particularly useful in separation logic, since holding ℓ ↦ → never precludes the existence of another share ℓ ↦ → ′ (unless is ⊤, in which case perm_of ( , ) is already Freeable).
We can now define the state interpretation assertion ( ) that connects program-logic assertions to a physical CompCert memory : In other words, our points-to assertions are "about" a physical whenever the logical heap they describe is coherent with .This allows us to derive rules in our separation logic about the interaction between points-to assertions and CompCert memory operations.For instance: Proof.We know that there exists a coherent with such that • * ℓ ↦ → .The rules of the heap algebra give us that ∃ ′ ⪰ .(ℓ) = YES( ′ , ).Then by coherence, we know that the permission at (ℓ) is at least Writable, so the storebyte operation is allowed.Next, by lemma 3.1, we know that it is a frame-preserving update to change (ℓ) from YES( ′ , ) to YES( ′ , ′ ).Thus, we have Finally, we must show that (ℓ := YES( ′ , ′ )) is once again coherent with storebyte( , ℓ, ′ ).The only changed condition from definition 3.2 is condition 1, and the new value of (ℓ) is exactly the new value of (ℓ), so coherence holds, and the proof is complete.□ In this fashion, even though our points-to assertions are not directly on CompCert memories, we can prove that operations in the logic correspond exactly to operations in the physical memory.As in Iris, but in contrast to VST, these theorems are proved entirely within our logic: we do not need to unfold the definition of "this assertion holds on a program state" or explicitly construct heaps that satisfy specific logical assertions corresponding to memory operations.The theorems also do not yet say anything about the semantics of any programming language; in section 4.1, we will use this theorem to prove a Hoare triple like {ℓ ↦ → } *ℓ = ′ {ℓ ↦ → ′ } connecting the effects of program statements to the logical heap.
The approach of defining a coherence relation between the logical heap and some other notion of memory, and including this relation in the definition of the state interpretation, is a simple but general technique for separating the structure used to model the points-to assertions in an Iris instance from the memory used in the operational semantics of the underlying language.We expect it to be useful for building Iris instances for languages with more complex models of memory, especially ones that already have a defined semantics whose physical memory is not just a map from locations to values.

Locks and Function Pointers
In older separation logics (including VST's), locks and function pointers were addressed using "predicates in the heap" [Gotsman et al. 2007], where assertions (lock invariants, function specifications, etc.) were included directly in heap resources, requiring some logical tricks (e.g., step-indexing) to break the circularity between the definitions of heaps and assertions.Iris provides a far more general solution via higher-order ghost state: cameras can include predicates freely as long as their operations respect step-indexing.As outlined in section 2.1, this lets us define global invariants as a special case of ghost state, and then access the resources in with a view shift.This makes it easy to define invariant-based locks without including invariants in the LK resource itself.Instead, the only argument to the LK resource is a boolean indicating whether the lock is currently acquired, and we define the lock assertion as: This invariant captures the idea that the resources are protected by the lock whenever the lock is not acquired2 .We address function pointers with a similar approach.We want to define an assertion ℓ ↦ → { }{ } saying that ℓ points to a function with precondition and postcondition .We do so by defining a camera of function specifications { }{ }, and setting up ghost state that maps each function pointer to its specification.We once again use the authoritative-fragment construction, giving us an authoritative resource •fs that contains the specifications of all declared functions, and a fragment •[ℓ := { }{ }] that knows the specification of an individual function.Then we can define the function pointer assertion as: This assertion connects the specification { }{ } to the FUN resource in the rmap, which in turn is connected by coherence to the location ℓ in the CompCert memory.For the share, we use the "discarded" share □, which is readable and arbitrarily duplicable, and guarantees that no other thread can change or deallocate the resource at that location.This is where it becomes crucial that the perm_of function used for coherence behaves differently for different kinds of resources: in the logic, ℓ ↦ → { }{ } involves a readable share of ℓ, but the corresponding permission in the CompCert memory will still be the unreadable Nonempty, preventing the program from actually performing load operations on ℓ.

SAFETY AND ADEQUACY 4.1 Weakest Preconditions
Iris builds a program logic on top of a language semantics by defining a notion of weakest precondition in the logic: The weakest precondition of is the smallest assertion guaranteeing that either is finished and satisfies the postcondition , or that given the state interpretation for a physical state , is not stuck and, for every ( 2 , 2 ) such that ( , ) steps to ( 2 , 2 ), we can re-establish the new state interpretation ( 2 ) and 2 's weakest precondition.The wp assertion is indexed by a set E of available invariant names, and the updates ⇛ E ∅ and ⇛ ∅ E allow the program to change ghost state and access the contents of invariants in each step, as long as all invariants are restored after the step.A Hoare triple { } { } E is then defined as − * wp E { }.
To define wp for our program logic, we need to instantiate this formula with CompCert's C semantics.The first question that arises is whether Clight's semantics fit into this framework at all.Most of the target languages for Iris are functional programming languages, with memory accesses and other operations of interest treated as side effects.Even in RefinedC [Sammler et al. 2021], function parameters and local variables are treated as bound variables that are substituted with the locations of passed parameters when called.Clight, in contrast, has an environment-based semantics, a more traditional way of describing imperative languages.A program state includes the name of the currently executing function, the next statement to execute, the memory, and two separate environments, one that maps stack-allocated variables to their addresses and one that maps temporary variables to their values.We resolve this by letting the "expression" contain the entire program state except the memory (which is held in ).
Beyond the expressions, there are two more semantic points that merit special attention.First, the updates ⇛ E ∅ and ⇛ ∅ E allow the semantics to access the contents of invariants in each step, as long as they are restored by the end of the step-but this is explicitly unsound in C! Suppose an invariant contained an assertion ℓ ↦ → : then this definition would allow two separate threads to write to ℓ without synchronizing, leading to a data race, which in C has undefined behavior (so we should not be able to prove anything about it).Existing Iris instantiations address this problem by either allowing races in the semantics (e.g., HeapLang), modifying the semantics so that race-sensitive operations occur over multiple steps (e.g., RustBelt [Jung et al. 2017]), or making the points-to assertion "subjective" so that one thread cannot use another's resources without synchronization (e.g., iGPS [Kaiser et al. 2017]/iRC11 [Dang et al. 2019]).None of these solutions are satisfying in our case: C semantics does not allow races, we want to reason about CompCert semantics directly rather than modifying it, and subjectivity is an idea from weak memory models that does not translate naturally into our sequentially consistent setting.Instead, we limit access to invariants by changing the inner masks on the updates from ∅ to E: Threads can still access the contents of invariants between program steps (e.g., by performing an opening view shift ⇛ E ∅ and then a closing view shift ⇛ ∅ E before resuming execution), but all the invariants in E must be enabled before we can take a program step, ensuring that we do not use resources from invariants to justify the step.
Second, CompCert's semantics includes a notion of external function with unknown semantics, specified by an arbitrary relation on the memories before and after the call.In VST, external calls are handled separately from the rest of the semantics: they operate on a special piece of external state that can only be modified by external calls.Prior work used this external state to connect to system calls provided by a verified operating system [Mansky et al. 2020], and also used external calls as a hook for a concurrency library [Cuellar et al. 2020].We can fit external calls into the formula above by letting the state be a pair ( , ) of the CompCert memory and external state .Internal steps leave unchanged; external steps may change both and according to their semantic relation.Our full state interpretation is then (( , )) ≜ ( ) * • (where is the fixed ghost name for the external state), and the corresponding assertion • allows the separation logic to track (but not modify) the current external state, supporting reasoning in the style of Mansky et al. [2020].
More inconveniently, external functions do not use the same form of nondeterminism as ordinary program steps.The specification for an external function ef has the form ∀ .{ ef ( )} ef () { ef ( )}, where the witness provides logical information that does not follow directly from the state-for instance, the witness may describe the intended invariant to be used when allocating a lock.When a program reaches an external call, it may choose any witness for the call such that ef ( ) holds, and then must be safe for all states satisfying ef ( ).This means that in the external-function case, our definition of wp must alternate angelic nondeterminism (existential quantification) and demonic nondeterminism (universal quantification)3 .Our final definition of wp is then: In the last disjunct, we existentially quantify over a witness satisfying the precondition ef , and then must be able to re-establish ( 2 ) and wp given any 2 satisfying the postcondition ef .As we will see in the next section, our desired safety property includes this same quantifier alternation for external calls, and so this definition is sufficient to guarantee that verified programs run safely.

Adequacy
The adequacy theorem of a program logic connects theorems proved in the logic (e.g., Hoare triples) with the semantics of the underlying programming language.For our logic, we want to know that if we prove a Hoare triple for a C program, then the program will execute correctly under CompCert's semantics.Most Iris developments use or specialize a generic adequacy theorem, based on Iris's definition of wp, that says that for any program , if we can prove {True} { .( )} where is a pure assertion (i.e., it is independent of program state), then 1) it does not get stuck and 2) if it terminates in a value , then ( ) is true.Because we have modified the definition of wp, we do not immediately obtain this adequacy theorem; furthermore, our ultimate goal is to re-prove the existing adequacy theorem for Verifiable C, which is structured to account for external calls: Definition 4.1.A program configuration ( , ) is safe for steps with postcondition , where is a predicate on CompCert memories and external states, if: • is halted and ( ) holds, or • ( , ) → ( 2 , 2 ) in CompCert's semantics, and ( 2 , 2 ) is safe for − 1 steps, or • = (ef (); 2 ) for an external function ef , and there is a witness such that ef ( , ) and for all 2 satisfying ef ( , 2 ), ( 2 , 2 ) is safe for − 1 steps.
Proof.By Löb induction.The definition of wp gives us three cases.In the first case, is halted in a state satisfying , so it is safe for steps trivially.In the second case, ( , ) steps in CompCert's semantics to some ( 2 , 2 ), and wp guarantees that after a view shift we have ( 2 ) * wp ⊤ 2 { }, so safety follows from the inductive hypothesis.In the third case, is of the form (ef ; 2 ), and there is some such that ef ( , ) holds; we must then show that ( 2 , 2 ) is safe for − 1 steps for all 2 satisfying ef ( , 2 ), which again follows from the definition of wp and the inductive hypothesis. □ As a side note, because the state includes external state, this definition of safety can be used to reason about the program's externally visible behavior: for instance, when is a networked server, we can instantiate the external state with an interaction tree describing a network protocol [Koh et al. 2019;Zhang et al. 2021], and the adequacy theorem implies that the server implementation complies with the protocol.Even though we do not reuse Iris's adequacy construction directly, the machinery of Iris still makes this proof much easier than in VST.Our proof of adequacy is 120 lines of Coq, while VST's is over 1000, involving explicit construction of rmaps corresponding to modified program states and extensive reasoning about the relationship between external function specifications and the program logic.All of this is avoided by using a separation-logic definition of wp and only exiting to meta-level reasoning after CompCert-level safety is established.

BUILDING THE PROGRAM LOGIC
Thus far, we have constructed a definition of weakest preconditions for CompCert C, the fundamental building block of Hoare triples.The next step is to build a program logic on top of this definition-a set of Hoare rules for the statements of Clight.We could consider reconstructing the proof rules of RefinedC [Sammler et al. 2021], or developing a new logic from scratch, but in our initial development we instead aim to reconstruct Verifiable C, the program logic of VST.Our proof rules will be syntactically identical to those of VST, but their meaning and the proofs of their soundness will be entirely different, building on the logic we have defined above rather than the original foundations of VST.

Proving the Rules of Verifiable C
As one might expect, the proof rules for C are more complicated than in most standard Hoare logics.For instance, the store rule, which might conventionally be {ℓ Aside from the fact that any writable share is sufficient to store to a location, we can see that 1) the LHS and RHS are expressions, not fully evaluated values, and 2) the rule includes typechecking conditions tc on both expressions, and, critically, these conditions are spatial assertions that may refer to memory resources.We find expressions in our rules because Clight uses a standard bigstep-for-expressions, small-step-for-statements semantics, and so the program * 1 = 2 does not go through a state of the form *ℓ = ′ in its execution; we could potentially define an equivalent semantics that uses small steps for expressions and decompose this rule further, but here we work directly on CompCert's semantics.The inclusion of tc conditions reflects a subtle point: in CompCert, the evaluation of expressions may depend on memory.This is not because expressions can perform memory accesses-in Clight, dereferences are broken out into separate statements-but because certain operations on pointer values (e.g., pointer comparison) have undefined behavior when their operands are unallocated pointers.Verifiable C reflects this with an enhanced type system that uses separation logic assertions to guarantee that the relevant pointers are allocated: ℓ ↦ → _ is sufficient to guarantee that ℓ is allocated, and so operations on ℓ will not get stuck.The tc( ) assertion collects the separation logic information needed to guarantee that can be evaluated without getting stuck4 , after which the memory-independent eval( ) function can compute the value that will return.Because the assertions in tc may overlap with eval( 1 ) ↦ → and may also require other memory, they are combined via a non-separating conjunction ∧ with both the points-to assertion and an arbitrary frame .
The theorem relating the memory-independent eval function to CompCert's evaluation relation eval CC is a good illustration of the contrast between the proofs of rules in VST and our new implementation of Verifiable C. The top-level theorem should say: Theorem 5.1 (informal).If tc( ) holds on a memory , then eval( ) = ⇔ eval CC ( , , ).
In VST, coherence between a CompCert memory and an rmap is defined outside of the logic, and so the theorem is stated formally as: Theorem 5.1 (VST).If tc( ) holds on an rmap coherent with a memory , then eval( ) = ⇔ eval CC ( , , ).
In our logic, we have already embedded coherence inside our state interpretation ( ), and so we can write the same theorem inside the logic: Theorem 5.1 (new).( ) * tc( ) ⊢ eval( ) = ⇔ eval CC ( , , ).
We need not mention the rmap ; it is implicit in the model of the separation logic.The proofs are similar-in either case, we use the fact that ℓ ↦ → _ holds on a logical heap coherent with to derive that ℓ is allocated in -but the new theorem statement is much easier to use inside the logic as we move forward.
This inside-the-logic approach carries forward to the proofs of the Hoare logic rules as well: Theorem 5.2 (store).The store rule is valid given our definition of wp.
Proof.We must show that Pre ⊢ wp * 1 = 2 Post, where Pre and Post are the pre-and postcondition of the Hoare triple for store.By the definition of wp, it suffices to show that for any state s.t.
Then by theorem 3.3 we have ( ) * eval( 1 ) ↦ → ⇛ ( ′ ) * eval( 1 ) ↦ → eval((typeof 1 ) 2 ), completing the proof.□ 5.1.1Function Pointers and Function Calls.One particularly interesting aspect of Verifiable C is its treatment of function pointers and function specifications.In section 3.3, we defined an assertion ℓ ↦ → { }{ } that associates a location ℓ with a function pre-and postcondition.However, this is not enough to guarantee that if we call ℓ in a state satisfying , it will return a state that satisfies .
For that purpose, we need to know that every function actually satisfies its specification, which is what we are trying to prove when verifying the program!Verifiable C approaches this problem in two parts.First, we reason about every function in the context of a set of function specifications fs, and the full definition of Hoare triple includes an assumption that every function in fs satisfies its specification: believe(fs) ≜ * (ℎ, , ) ∈fs Second, we carry an assertion that connects each function-pointer assertion to the set fs: where symb is a function constructed from the program that maps each function name to its location in memory (more on this in section 5.2).This assertion tells us that every function pointer corresponds to some defined function with a specification in fs, and every function specified in fs has a corresponding function pointer in memory.(This means that every function pointer must point to a globally declared function, which would not necessarily be the case in a language with anonymous functions, but is the case in C and is explicitly required by CompCert's semantics.)Both funs_valid and believe are then included as assumptions in our definition of Hoare triples: Every rule in the program logic is universally quantified over fs.In the top-level soundness proof, the believe assumption is discharged through Löb induction, while the funs_valid assertion is part of the precondition of the main function (which we discuss in detail in section 5.2).Taken together, the believe and funs_valid assertions let us convert a function pointer assertion ℓ ↦ → { }{ } into the knowledge that ℓ corresponds to a defined function that satisfies { }{ }.This lets us prove the expected Hoare rule for function calls: As long as evaluates to a function pointer with precondition and postcondition , we can call it with that specification; we do not need to prove anything else about in the call rule itself.
As a side note, RefinedC [Sammler et al. 2021] also has a function pointer assertion, but instead of describing the function's pre-and postcondition it includes the function body directly, and the user separately proves that that body satisfies a specification as needed.We have not yet done a thorough comparison of the advantages and disadvantages of these two approaches, but for now we use the specification approach for compatibility with Verifiable C.

Analysis.
We were able to re-prove all of the rules of Verifiable C in our new logic, with their statements unchanged up to differences in notation between Iris and VST.This is encouraging: it means that everything proved in VST should also be provable in our logic.Furthermore, by doing our reasoning entirely within the program logic and taking advantage of Iris's tactics, the proofs of these rules are much simpler and easier to maintain.Discounting derived rules, Verifiable C has 14 distinct surface-level proof rules-comparable to any other program logic-but their proofs are quite complicated: in the VST development, they comprise ∼12310 lines of Coq code.We prove the same rules in ∼7060 LoC in our new logic, a 43% decrease.

Initial State and Whole-Program Correctness
For simple programming languages, we may use emp as the precondition of a whole program, and then allocate all the state we use inside the proofs of the relevant functions.In C, however, some parts of the program state are initialized before the program enters the main function, and we must provide the corresponding assertions in the precondition of main for the user to be able to reason about them.There are three kinds of resources that must be present in the initial state: the memory assertions for global variables and function pointers, the table of specifications for defined functions, and a reference to the external state.The external state is just an arbitrary piece of authoritative ghost state, but the other two require more complex reasoning: we must describe an initial logical heap that is coherent with CompCert's initial memory state, and then transform it into a collection of points-to assertions for the program's global variables and function pointers.
We begin by defining a translation from CompCert memories to corresponding logical heaps.Since CompCert's permissions each correspond to a wide range of shares, we pick default shares , , and to represent arbitrary writable, readable, and nonempty permissions.Then we can construct a logical resource for each memory location as follows: The logical heap for a memory is then simply rmap_of ( ) ≜ ℓ. res_of ( , ℓ), which is coherent with by construction: Theorem 5.3.rmap_of ( ) is coherent with .
Proof.By the definition of res_of, each location with a value in rmap_of ( ) has the same value in , and we have chosen shares such that perm_of (rmap_of ( )(ℓ)) is exactly the permission of (ℓ) for each ℓ.(In particular, recall that CompCert always assigns Nonempty permission to function pointers, and perm_of (YES( , FUN)) = Nonempty.)□ This gives us a separation logic assertion that describes an initial memory 0 -namely, •rmap_of ( 0 ) -but it is not a particularly useful assertion: it monolithically describes the entire logical heap, instead of giving us ownership of each individual allocation.Our next step is to translate it into a collection of points-to assertions for the initialized memory.A CompCert C program defines a collection of global variables vars( ) of the form ( , ), where is the identifier of the global variable and is its initial value, and a collection of functions funs( ); the initial memory init_mem( ) allocates a memory block for each of these identifiers according to a mapping symb from identifiers to locations.
Proof.By induction on the list of definitions in .For each defined identifier , init_mem( ) initializes the block symb ( ) with the appropriate data (the initial value for a global variable, Undef for a function pointer).So rmap_of (init_mem( ))(symb ( )) contains the resources corresponding to that data, and by splitting those resources from •rmap_of (init_mem( )), we can construct the appropriate assertion, symb ( ) ↦ → for a global variable or symb ( ) ↦ → □ FUN for a function pointer.Finally, in the function pointer case, we use a view shift to add the function's specification to the authoritative set of specifications and obtain a fragment assertion •[symb ( ) := fs( )] , and then combine symb ( ) ↦ → □ FUN and •[symb ( ) := fs( )] into the function pointer assertion symb ( ) ↦ → fs( ).□ Combining these two theorems, we can conclude that the initial memory of a program is coherent with the collection of points-to assertions for its initial global variables and function pointers.
We include the global-variable assertions in the precondition for the main function (collected in the assertion globals( )), and use the function assertions to establish the funs_valid condition of section 5.1.1,guaranteeing that every specified function is present in memory and vice versa.
Then in the proof of a program, we start with access to the global variables and functions of the program, and can reason about them in the same way as with memory allocated during the program.
Combined with the adequacy theorem of section 4.2, this gives us the following top-level safety result: Theorem 5.5 (Whole-Program Adeqacy).Let fs be a set of specifications for the functions in a program , where fs(main) = {globals( ) * }{True}.Then if ⊢ ({ } { } with fs) is provable for each specification ( , , ) ∈ fs, we can conclude that ( , init_mem( ), ) is safe for any number of steps.
Proof.By theorems 5.3 and 5.4, we can start from emp and use view shifts to allocate ghost state satisfying both the initial state interpretation (init_mem( ), ) and the precondition of main.Instantiating the Hoare triple for main then gives us (init_mem( ), ) * wp ⊤ main {True}.By theorem 4.2 (adequacy), this implies (∀ .( , init_mem( ), ) is safe for steps), as desired.□

VERIFYING C PROGRAMS
1 struct list { unsigned head ; struct list * tail ;}; 2 3 struct list * reverse ( struct list * p ) { 4 struct list *w , *t , * v ; To demonstrate the use of our logic, we reconstruct most of VST's Floyd automation library [Cao et al. 2018] on top of it, and use the Floyd tactics to verify some of VST's example programs.Figure 4 shows the code for one of these examples, a linked-list reverse function.While it is quite simple as C programs go, it does exercise C-specific features like structs and field accesses, as well as standard separation logic pointer manipulation and loop invariants.The complete proof of the reverse function is shown in figure 5. We use semax_body to assert a Hoare triple on the Clight representation of the function, f_reverse, using a pre-and postcondition defined by reverse_spec (not shown).Vprog collects global variable information from the C program, and Gprog holds the set of defined functions and their specifications (fs in section 5.2).unfold listrep at 2; Intros ; subst ; contradiction .

Fig. 5. The proof of the reverse function
The proof is not complicated, but it uses several tactics from VST's Floyd automation library: start_function to unfold the function definition and its specification, forward to symbolically execute straight-line code, forward_while to provide a loop invariant, Intros and Exists to manipulate separation logic quantifiers, and entailer!to automatically solve separation logic entailments.Our implementations of these tactics are fundamentally unchanged from VST's version of Floyd [Cao et al. 2018], but almost every supporting lemma and tactic needed at least slight changes to work with our Iris-based definitions.Once these changes were made, the VST proof script for the function body worked almost verbatim in our system: the only changes are the use of Iris's ∃ in place of VST's notation, and the addition of the invariant mask ⊤ in the arguments of semax_body, reflecting the fact that our Hoare triples are now parameterized by masks, as in Iris.We have reconstructed 13/20 of VST's basic example programs (https://github.com/PrincetonUniversity/VST/tree/master/progs64) in this way, and we expect that the rest can be reconstructed similarly-they are blocked only by various bugs in our reimplementations of the Floyd tactics, which are time-consuming but not difficult to solve.Table 1 summarizes the examples, their status, and the features of C they cover.
There are two categories of example programs whose translation to our new logic is more interesting: those that benefit from using Iris tactics in place of VST's existing automation, and those that used VST's prior ad-hoc ghost state implementation [Mansky 2022] and can now use Iris cameras instead.The latter examples are exclusively concurrent programs, which are out of the scope of this paper, but the former category already gives us a taste of the benefits of joining VST and Iris.For instance, VST's object example implements an object-oriented dynamic dispatch pattern in C using structs and function pointers: each object has a method table that contains pointers to its implementation of two methods defined in an interface.In the proof of this program, the representation predicate for the mtable field contains assertions about the two function pointers in the interface, whose specifications are parameterized by an underlying object implementation : Because the share of the mtable pointer is existentially quantified and function pointers are persistent, this assertion can be duplicated, i.e., object_methods( , ) ⊢ object_methods( , ) * object_methods( , ).In VST (see the left-hand side of figure 7), proving this involves explicitly rewriting with a lemma saying that function pointers can be duplicated, and then using the automated entailment solver entailer.Iris, on the other hand, has built-in support for duplicating and canceling persistent assertions; an invocation of Iris's iIntros tactic with the #$ pattern (# for persistence, $ for cancelation) automatically matches one ↦ → assertion on the left-hand side with any number on the right-hand side.When we encounter proof goals that directly manipulate separating hypotheses, manage persistent assertions, etc., we can immediately take advantage of Iris tactics that support these kinds of reasoning, simplifying our proofs.(1) can prove anything that can be proved in VST, with equally strong guarantees about the behavior of verified programs; (2) is easier to maintain and extend than VST; and (3) allows users to write simpler proofs in some cases by taking advantage of Iris tactics and automation.In this section, we summarize the evidence for these assertions, give an overview of the Coq implementation of our logic, and discuss further evaluation of the system that we or others could perform.
For assertion 1, we observe that, as described in section 5.1.2,the rules of our program logic are exactly the same as VST's (up to difference in notation).This tells us that on paper, every C program that can be verified in VST can also be verified in our logic.In practice, as we have seen  in section 6, some VST proofs do not immediately work in our system, but this is only because of bugs in our reimplementation of the automation, and we can be sure that once we fix these bugs all prior verifications will work again.Furthermore, we prove adequacy (Theorem 4.2) against exactly the same definition of safety as VST, so the guarantees we obtain about verified programs are exactly the same as in VST.This rules out the possibility that, for instance, we have proved the same assertions about a program but subtly changed the meaning of those assertions in a way that makes the proof useless.In fact, we have changed the meaning of assertions by replacing their foundations, but our adequacy result shows that this does not in any way weaken the properties we prove about C programs.
For assertion 2, while we have not yet demonstrated a substantial extension of the logic (e.g., with transfinite step-indexing or later credits), we have certainly seen that it is easier to maintain.As mentioned in section 5.1.2,the soundness proofs of the logic are more than 40% smaller than in VST (we give more statistics about our Coq development in section 7.1).Those proofs are also performed at a higher level of abstraction than in VST, so they should be unaffected by minor changes in the foundations of the logic.On the foundational side, using Iris instead of an ad-hoc separation logic means that we automatically benefit from changes to base Iris: in fact, our logic already supports arbitrary higher-order ghost state and invariants without any additional effort, although these features are most easily demonstrated on concurrent programs, so we will not discuss them further here.Finally, the object example in section 6 demonstrates that users can benefit from Iris Proof Mode's tactics and automation when verifying C programs in our logic.While the full power of Iris's automation comes into play primarly in concurrency reasoning, its support for named premises, persistent assertions, and Coq-style patterns is already useful in the domain of our current logic.

Coq Development
The work described in this paper is fully formalized in Coq.Our development currently uses ORA [Krebbers et al. 2018], a fork of Iris's logic that allows linear (i.e., non-garbage-collected) resources, but this is not fundamental: while intuitively a malloc-free language should treat memory assertions linearly, it is well known that linearity alone is insufficient to prove absence of memory leaks, and the of Iron [Bizjak et al. 2019] can be used to prove absence of memory leaks even in an affine separation logic.Our development is structured as a branch of VST, with our contributions focused in two folders: veric, the Verifiable C program logic, and floyd, the automation library.We remove most of MSL, VST's original generic formulation of separation logic, keeping only a few standard-library lemmas and tactics, and the definition of tree shares and their associated algebra.We also add a folder shared that generically defines the camera of shared values from section 3.1; this folder only imports files from Iris, and could be made into a separate repository or added to Iris in the future.In Verifiable C, we add one new file instantiating the new camera and modify 44 of the remaining 75 files, reflecting the new definitions of coherence and program logic assertions, and the entirely new proofs for all Hoare rules and supporting lemmas.In Floyd, we modify 66 of 91 files, re-proving supporting lemmas and adapting tactics to the Iris implementation of Verifiable C. As described in section 5.1.2,our development of Verifiable C is significantly smaller than VST's: due to the use of Iris infrastructure and tactics and the shift from model-level to logic-level proofs, our overall veric development is ∼44k LoC, compared to ∼67k LoC in VST.Our development of Floyd, on the other hand, is roughly the same size as VST's.
7.2 Future Evaluation: Integrating Iris-Based Tools with VST While we have demonstrated that our new system can verify the same programs in the same way as VST (and in some cases slightly more easily using Iris tactics), we have not yet evaluated the potential of our system to go beyond VST and verify new C programs in new ways.The most immediate benefit we expect is the ability to to adapt existing Iris tools and frameworks to our new Verifiable C. For instance, RefinedC [Sammler et al. 2021] is a refinement-and-ownership type system for C programs with foundational semantics in Iris, allowing users to write type annotations on C programs and semi-automatically obtain foundational correctness proofs.Reimplementing this type system on top of our logic would be a powerful demonstration of the advantages of an Iris-based VST, and would allow semi-automatic foundational verification of CompCert C programs.We do not yet know how much work will be involved in translating the definitions of RefinedC's types from its separation logic to Verifiable C-our work has bridged the foundational gaps, but there are still various differences in the interface and implementation of points-to assertions, values in memory, etc., that make the translation nontrivial.
As another example, ReLoC [Frumin et al. 2021] is a system for proving contextual refinement properties in Iris's logic, targeted to the simple functional language HeapLang.To prove that an implementation program refines a specification program (i.e., ⪯ ), ReLoC builds ghost state that represents the state of in the proof of , and provides proof rules and tactics for relating the steps of the two programs.By adapting the same ghost state to Verifiable C and reconstructing the proof rules and tactics, we could build a system for proving that CompCert C programs refine HeapLang programs, so that verification could be decomposed into a pass dealing with the details of C and a pass dealing with the algorithm at a higher level of abstraction.Integrating the two systems would be a good test of whether our divergences from the standard construction of an Iris logic cause problems for other Iris-based tools.

RELATED WORK 8.1 Program Logics for C
There are several separation-logic-based verification tools for C programs.The most obviously related is VST itself, which has been used to verify cryptographic functions [Beringer et al. 2015], web servers [Zhang et al. 2021], and simple concurrent programs [Mansky et al. 2017].Prior work by Mansky [Mansky 2022] integrated some features Iris into VST, which both increased the power of the logic and highlighted the limitations of working within VST instead of moving to Iris foundations: the system could express only limited higher-order ghost state, and the proofs of program logic rules became more complicated with each feature added.We believe that building up from Iris's foundations is the more theoretically satisfying and the more practically effective approach, and hope that our work will lead to a new version of VST that is closely integrated with advances from the Iris community.
Other separation logic verifiers for C include VeriFast [Jacobs et al. 2011] and VerCors [Blom and Huisman 2014].VeriFast in particular has integrated some Iris-style reasoning for fine-grained concurrent programs.Both systems use the common approach of reducing program correctness to a collection of first-order proof obligations that can (often) be automatically proved by SMT solvers, which makes them much easier to use but much less foundational: in fact, both systems use essentially the same program logic for both C and Java, with no underlying formal semantics for either language.

Iris Instances for Real Programming Languages
We know of four Iris instances that target some fragment of a real-world language.RustBelt [Jung et al. 2017] is built on Rust , a core calculus for Rust that models concurrency and lifetimes.The Goose language used in Perennial [Chajed et al. 2019] automatically translates a subset of Go to a similar core calculus, with the advantage that source code can actually be written in Go (but there is still no formal connection between the translated lambda-calculus and executing Go code).Iris-Wasm [Rao et al. 2023] is the Iris instance that comes closest to giving foundational guarantees on real code, by connecting to WasmCert-Coq [Watt et al. 2021], a Coq translation of the official formal specification of WebAssembly.WasmCert-Coq has an executable interpreter, so programs verified with Iris-Wasm can actually be run.
The most directly relevant instance to our work is RefinedC [Sammler et al. 2021], which translates C to a core calculus, Caesium, based on the Cerberus reference semantics [Lau et al. 2019].While CompCert aims to implement C more or less according to the ISO specification, Cerberus and Caesium aim to model C as it is used in writing systems code, which means that some operations (especially complicated pointer arithmetic) are undefined in CompCert but defined in Caesium; Caesium also includes some concurrency features (sequentially consistent atomic operations), which are outside the scope of CompCert.Cerberus is a partially operational model that uses SMT solvers to compute sets of allowed executions, which would make it difficult to connect to a verified compiler.The connection between Caesium and Cerberus is also not formal, so Caesium programs cannot themselves be executed.One interesting avenue unlocked by our work is to rebuild RefinedC on top of Verifiable C, enabling semi-automatic generation of foundational correctness proofs for CompCert C programs.

CONCLUSIONS AND FUTURE WORK
Iris has already been used to build expressive program logics with strong foundational guarantees of correctness; those guarantees are even stronger when we can connect them to verified implementations of real programming languages.We have demonstrated the construction of a program logic for CompCert C within Iris, using entirely Iris-based foundations but providing all the same surface rules and adequacy guarantees as VST/Verifiable C. CompCert presents several challenges that have not been addressed in prior Iris developments, including permissions in the physical memory, nonempty unreadable ownership, and mixed nondeterminism for external function calls, all of which we solve here.Our logic can be used to apply all the machinery and technical innovations of Iris to prove correctness of sequential C programs compiled with CompCert.
One of main benefits of an Iris-based CompCert logic is that it can be more easily integrated with other Iris developments.Our immediate future work is to integrate some of Iris's tools for automation, which promise to allow foundational verification to scale to much larger programs.The most directly relevant work is RefinedC [Sammler et al. 2021], which defines a separation-logicbased type system for C programs, albeit a slightly different subset of C with a slightly different semantics.RefinedC's type system is built on top of a generic framework called Lithium, and we should now be able to build a Lithium-based type system for Verifiable C, letting us semiautomatically obtain correctness proofs for CompCert C programs.Language-independent proof automation tools like Diaframe [Mulder et al. 2022] are also tempting targets for integration.Apart from automation, the ReLoC refinement proof system [Frumin et al. 2021] allows relational proofs in Iris, and could be useful both for proving security properties (e.g., information flow control) on C programs, and for refining C programs to more easily verified functional programs.A logic for CompCert C connected to the Iris ecosystem promises to vastly expand the ease of verification of real C programs and the kinds of properties we can prove.
We are also interested in the possibility of comparing the VST and Iris approaches to proof automation head-to-head.In VST, the resources available at each point in a program are treated as a "soup" that is automatically searched and modified by symbolic execution tactics, but is difficult for the user to interact with directly.Iris, on the other hand, maintains separation logic premises in a "context" similar to Coq's context of hypotheses, with names for each individual assertion and tactics for manipulating them using those names.Our system allows users to enter Iris Proof Mode and name and manipulate assertions in individual entailments, but still uses VST's top-level symbolic execution, so the Iris context is lost between steps.We would also like to develop an Iris-style tactic interface for our logic, based on existing Iris instances' wp tactics, where we maintain a separating context across symbolic execution steps.This would be a more familiar interface for Iris users, and would also take better advantage of Iris's support for persistent assertions, hypothesis manipulation, etc., but possibly at the cost of VST's sophisticated automation for finding and modifying assertions via symbolic execution-for instance, when executing a struct field access, VST can often automatically find the points-to assertion for the struct and modify precisely the piece of it corresponding to the field being accessed.Once we can use both sets of tactics side by side over the same logic, we can directly compare them and figure out where each might benefit from the other's techniques.

Towards a Concurrent Separation Logic for CompCert C
As mentioned in the introduction, while Iris separation logics are naturally concurrent, CompCert's semantics are strictly sequential.This means that while we could easily write a concurrent version of the separation logic presented here, it would be quite difficult to prove or even state its adequacy theorem-we would find it hard to precisely define what guarantees we get from verifying a concurrent C program.In fact, even the semantics of concurrent C programs is currently unclear: there are extensions of CompCert with specific concurrency models [Ševčík et al. 2013], and VST has an extension for well-synchronized lock-based concurrency [Cuellar et al. 2020], but neither one captures the general concurrency model of the C language.The latter framework is also both extremely complicated (involving 5 different layers of semantics and modifications to internal CompCert proofs) and unfinished: several discrepancies between CompCert and the various semantic layers have not been resolved, and the Coq development has been very difficult to maintain.We have begun work on reconstructing VST's concurrent extension, aiming to prove adequacy against the same concurrent extension of CompCert, and expect to again obtain simpler proofs by working within the logic and taking advantage of Iris features; but in the long run, we believe that a cleaner and more generic approach needed.Writing a concurrent separation logic for a lifted version of a sequential programming language is an interesting problem in general, and we are currently investigating techniques such as CASCompCert [Jiang et al. 2019] and DimSum [Sammler et al. 2023] that may allow cleaner decomposition of the various components of the proof.

Fig. 3 .
Fig. 3.The organization of the Verified So ware Toolchain

Table 1 .
VST's basic examples and their status in our system