Quiver: Guided Abductive Inference of Separation Logic Specifications in Coq

Over the past two decades, there has been a great deal of progress on verification of full functional correctness of programs using separation logic, sometimes even producing “foundational” proofs in proof assistants like Coq. Unfortunately, even though existing approaches to this problem provide significant support for automated verification, they still incur a significant specification overhead: the user must supply the specification against which the program is verified, and the specification may be long, complex, or tedious to formulate. In this paper, we introduce Quiver, the first technique for inferring functional correctness specifications in separation logic while simultaneously verifying foundationally that they are correct. To guide Quiver towards the final specification, we take hints from the user in the form of a specification sketch, and then complete the sketch using inference. To do so, Quiver introduces a new abductive deductive verification technique, which integrates ideas from abductive inference (for specification inference) together with deductive separation logic automation (for foundational verification). The result is that users have to provide some guidance, but significantly less than with traditional deductive verification techniques based on separation logic. We have evaluated Quiver on a range of case studies, including code from popular open-source libraries.


INTRODUCTION
The problem of how to verify functional correctness of large, stateful programs is one of the oldest challenges of computer science, tracing back to the work of Hoare [24] and Floyd [21].Over the past two decades, remarkable progress has been made following the advent of separation logic [41,49], an extension of Hoare logic that supports modular reasoning about stateful resources.Based on the foundation of separation logic, a number of powerful deductive verification tools have been built, including VeriFast [27], CFML [11], Bedrock [12], GRASShopper [43], VST [2,7], Viper [39], Gillian [53,36], Perennial [8], RefinedC [52], and CN [45].They provide exceptionally strong verification guarantees (e.g., memory safety and functional correctness in a pointer-manipulating new verification approach, abductive deductive verification, which integrates ideas from abductive inference with deductive separation logic verification to infer specifications in separation logic. Automating separation logic.For deductive separation logic verification and automation, we follow in the footsteps of RefinedC [52].RefinedC is a recently developed separation logic verification technique for establishing functional correctness of C code.Its distinguishing feature is that it is foundational and, additionally, automated: Embedded into the Coq proof assistant, RefinedC (1) provides powerful automation of separation logic, (2) inherits support for a large variety of functional correctness reasoning from Coq's ambient meta-logic, and (3) is proven sound against Caesium, a detailed model of the C semantics in Coq.For Quiver, we take inspiration from RefinedC's approach to separation logic proof automation (i.e., goal-directed proof search for weakest preconditions; see §2), its separation logic-based type system for handling the complexities of C, and its embedding into Coq to support a large variety of mathematical theories.
As mentioned above, a weak spot of RefinedC is that it-like other deductive verification techniques-requires considerable amounts of specification.To illustrate this point, let us consider a poster child example for specification inference: The functions xmalloc and xzalloc are simple helper functions for wrapping memory allocation in C (inspired by similar wrappers in popular open source projects [15,22,48]).They encapsulate common patterns such as (1) handling the case that allocation fails and malloc returns NULL (xmalloc) and ( 2) initializing freshly allocated memory with zeros (xzalloc).The implementations of the two functions are dead simple.Yet, when verifying them in RefinedC, we end up writing more lines of specification (Lines 1-5 and Lines 10-14) than there are lines of code.And for no good reason: as we will see below, the specifications of xmalloc and xzalloc can be inferred from those of malloc, memset, and abort.
Abductive inference.A key building block for us in reducing the specification burden is the idea of abductive inference in the sense that we infer the specification for a piece of code by "puzzling together" existing specifications for its component parts.To illustrate this idea, let us assemble the specifications of xmalloc and xzalloc from the auxiliary operations malloc, memset, and abort.The specifications of all operations are depicted in Fig. 1. (For simplicity, we phrase these specifications in a separation logic instead of RefinedC's type system.)The operation malloc takes a size_t integer  and returns either NULL or a pointer ℓ to a memory block of size  (denoted block ℓ ) whose contents  are uninitialized (denoted uninit(, )).The operation memset, called with zero, initializes the contents of a pointer with zeros (denoted zeros(, )), and the operation abort never returns (postcondition False).Using these specifications, we can assemble the specification of xmalloc (and analogously xzalloc) as follows: The precondition  ∈ size_t is inherited from malloc.The postcondition is derived from the post of malloc knowing that, in the NULL-case, we never return due to abort.
The idea of using abductive inference in separation logic is not new.It was first pioneered by bi-abduction [5,6], a landmark technique for compositional shape analysis based on separation logic.Bi-abduction is one of the cornerstones of Meta's Infer tool for detecting bugs in millions of lines of code [4] and also inspired a line of research on bug finding using incorrectness logic [40,47,32].It takes as input the code of a function and generates a separation logic specification that summarizes the footprint of the code via abductive inference.However, in the interest of supporting "pushbutton" automation, bi-abduction focuses on fixed, restricted fragments of separation logic.For example, the original work of Calcagno et al. [5,6] restricts attention to points-to assertions ℓ ↦ → v, list segments lseg(ℓ, r), and equalities v = v ′ .As such, it cannot express-or abductively infer-for example, the specifications of xmalloc and xzalloc in Fig. 1, since they go beyond this fragment. 1  Abductive deductive verification.With Quiver, we pursue a fundamentally different approach.Rather than trying to build push-button automation by restricting the separation logic fragment, we instead aim to integrate abductive inference into deductive verification approaches that already handle rich fragments of separation logic.To do so, we introduce a new technique we call abductive deductive verification.The main abductive deductive verification judgment Δ * [] ⊢ wp  {Φ} marries deductive separation logic verification, via the weakest precondition connective wp  {Φ}, with abductive inference of a precondition , via the abduction judgment Δ * [] ⊢ .Concretely, deriving Δ * [] ⊢ wp  {Φ} corresponds to deductively verifying the expression  in the context Δ while, simultaneously, abductively inferring any missing resources  that are needed to do so.By combining both styles of reasoning, we maintain the ability to deductively verify programs with rich separation logic specifications while additionally benefiting from the advantages of specification inference (e.g., inferring the specification of xmalloc while verifying it; see §6.2).Specification sketches.Since Quiver targets rich separation logics, fully automatically puzzling together specifications is not always the right choice (or even feasible).Consider the following extension of the previous example-a function that allocates a vector initialized with zeros: (For now, we ask the reader to ignore the annotation "[[q::...]]".)A standard functional correctness specification for mkvec would be { ∈ int *  ≥ 0} mkvec() {v.vec(v, 0  )} where vec(v, xs) is an abstract predicate for vectors with contents xs (a list of integers) and 0  is a list filled with  zeros.If we simply "puzzle together" a specification for mkvec based on the specifications of xmalloc and xzalloc (in Fig. 1), we would arrive at a low-level specification in terms of points-to assertions and the zeros-predicate-not a high-level specification about vectors.The underlying issue is that a single function can have multiple specifications at different levels of abstraction-depending on the intent of the developer.To guide the inference to the desired one, we thus use specification sketches.
Guided specification inference à la Quiver.That is, Quiver explores the middle ground in between (a) taking a complete specification as user input and verifying the code (as in RefinedC) and (b) taking only the code as input and inferring the entire specification (as in bi-abduction).We take a specification sketch as input, use it to resolve ambiguity, and complete it to a full specification-but without requiring the user to provide every little detail.Quiver works in three steps: (1) Data type declarations.First, the user defines their custom data types that are used in the code (e.g., arrays, linked lists, maps, buffers, vectors, etc.).This step includes choosing mathematical domains, imposing invariants on values, and relating mathematical and physical representations.(2) Function specification sketches.Second, the user can provide sketches for functions.These sketches are similar to separation logic specifications (e.g., describing the abstract predicates for arguments).There is, however, a crucial difference: they are incomplete with holes for, e.g., arguments of abstract predicates, additional constraints, and missing ownership.(3) Specification inference.Finally, Quiver takes this sketch and completes it into a specification for the entire function using abductive deductive verification.This includes adding missing preconditions, making imprecise annotations precise, adding constraints for unspecified function arguments, and figuring out the postcondition of the function.
In the resulting system, users control how much specification they want to provide.By default, if the inference is successful, the resulting specification closely follows the code.If the user decides to "sprinkle in" some annotations that constrain function arguments or local variables to a certain data type, Quiver takes these into account and adjusts the specification accordingly.And if the user provides the complete function specification, Quiver turns into a traditional technique for verifying functional correctness.For example, the specifications of xmalloc and xzalloc can be derived fully automatically without any sketches.For mkvec, we only add the sketch in Line 22: it instructs Quiver that the return value is a vector, which results in the high-level vec-specification.

Contributions.
Our key theoretical contribution is Abductive Deductive Verification ( §2), which provides a powerful basis for specification inference in rich separation logics.With the abductive deductive verification judgment Δ * [] ⊢ wp  {Φ}, we marry traditional deductive verification via the weakest precondition wp  {Φ} with abductive inference via the abduction judgment Δ * [] ⊢ .Our key technical contribution is that we realize abductive deductive verification in the form of Quiver, which consists of four parts: • The abduction engine Argon ( §3), which automates the abduction judgment Δ * [] ⊢ .Its key contribution is a goal-directed proof search procedure for abductive inference.It supports predicate-transformer style reasoning, necessitated by the weakest precondition wp  {Φ}, provides extensible proof search, and has powerful support for instantiating existential quantifiers.• The type system Thorium ( §4), which uses types in separation logic (à la RefinedC) to scale automated reasoning about the weakest precondition wp  {Φ} to the complexities of C. Its key contribution is that it works under incomplete information about the proof context Δ, meaning it works even when the types that are supposed to guide the proof search are yet to be determined.• A proof-of-concept Implementation ( §5) in the Coq proof assistant [13]-with a frontend for C, building on Iris [29] and RefinedC's Caesium semantics for C. The implementation infers specifications and, at the same time, proves them correct in Coq, which includes proving the absence of out-of-bounds accesses, use-after-free, and integer overflows.• An Evaluation ( §6), applying Quiver to several interesting case studies, including a dynamicallyallocated vector data type and code from popular open-source libraries.
We provide the implementation and all inferred specifications in the accompanying Coq development, along with an appendix containing further details on the inferred specifications [56].Limitations.Quiver does not infer loop invariants, but supports manually provided invariants.Quiver does not infer specifications of function pointers and only handles sequential code.Moreover, while Quiver builds on the detailed Caesium C semantics, Quiver does not handle all features of C. In particular, it neither enforces that pointer accesses are aligned, nor supports unions, nor supports integer-pointer casts, and it inherits the limitations of Caesium (e.g., no floating point numbers).

KEY IDEA: ABDUCTIVE DEDUCTIVE VERIFICATION
In this section, we explain our approach of abductive deductive verification.Concretely, we discuss the abductive deductive verification judgment Δ * [] ⊢ wp  {Φ} (in §2.1), the treatment of existential quantification (in §2.2), and how we steer the inference via specification sketches (in §2.3).To avoid getting bogged down in the details of C, we focus on a simple, expository language  expo for this explanation.From §3 onwards, we will then explain how we scale the approach to actual C code.
A running example.The language  expo is depicted in Fig. 2. It is a simple, substitution-based language with heap-allocated data structures, modeled as mutable, finite maps  from fields to values (similar to objects in JavaScript).We write dom  for the fields of  and  for the empty map.Values v can be locations ℓ and integers .The expression new() allocates an empty struct, . ←  ′ assigns  ′ to the field  of , and . dereferences field  of .We abbreviate  1 ;  2 ≜ let =  1 in  2 .
To reason about  expo in separation logic, we use resources and pure assertions.The resources are points-to assertions ℓ ↦ → , which assert ownership of a struct at location ℓ with at least the fields , and abstract predicates P(v, ì ) (see §2.3).The pure assertions include loc(v, ℓ) for "v is the location ℓ" and int(v, ) for "v is the integer "; they state facts without ownership and can thus be duplicated.
To keep matters concrete, we focus on an example in  expo , a data type for integer ranges [, ).This range data type is represented by a struct with two integer fields: s (for the start of the range) and e (for one past the end of the range).We define three operations operating on ranges, depicted in Fig. 3: init for initializing a previously allocated range r with bounds a and b, mk_range for allocating and initializing a new range from a to b, and size to determine the size of a range r.

The Essence of Abductive Deductive Verification
We start by explaining the abductive deductive verification judgment Δ * [] ⊢ wp  {Φ}.It consists of two parts: a separation logic weakest precondition wp  {Φ} [17,29,28] and an abduction judgment Δ * [] ⊢  where Δ is a separation logic context,  is an additional inferred precondition, and  is the current goal.The basic idea is that when we derive Δ * [] ⊢ wp  {Φ}, we prove that Δ together with  is a sufficient precondition for  to satisfy the postcondition Φ.We explain how Δ * [] ⊢ wp  {Φ} works with the rules in Fig. 4. To stage the explanation, we present it in three steps, moving from verification to inference.In Version 1, we use the judgment for ordinary verification-without any inference-and explain the proof search strategy underlying our automation: goal-directed proof search for weakest preconditions.In Version 2, we extend the judgment to infer preconditions, by incorporating abduction into our goal-directed proof search.In Version 3, we extend it further to infer complete specifications.In this last version, we explain why we infer so-called "predicate transformer specifications" instead of Hoare triples.
To keep matters concrete, we focus on the operation init.For now, we ask the reader to ignore the blue assertion in the code of init (in Fig. 3).We will infer the following specification for init: The precondition assumes thatv r is a location ℓ and that this location ℓ stores a struct with contents .The postcondition ensures that ℓ stores an updated struct with v a in its s-field and v b in its e-field.
Version 1: Deductive verification.Say we are given the spec We aim to deductively verify it via the judgment Δ * [] ⊢ wp  {Φ}, where we instantiate Δ ≜  init , Φ ≜  init , and  with the body of init(v r ,v a ,v b ), and for now we ignore the precondition  (pretend it is True).
Our proof strategy for deductive verification-following in the footsteps of RefinedC [52]-is to employ goal-directed proof search for weakest preconditions.It is goal-directed in the sense that, to derive Δ * [ ] ⊢ , we iteratively inspect the current goal  and then apply a matching rule that transforms  into a new goal  ′ .(If multiple rules match, we use the first one whose side conditions can be proven; the order is described in the figures.)And to reason about weakest preconditions, we use the rule abd-embed to embed deductive proof rules of the form " ⊣  when ": here,  is the weakest-pre goal we are trying to solve, and  is the new subgoal that implies it under the (optional) side condition .In the case of wp (v r .s← v a ; v r .e← v b ) { . init }, we start by using abd-embed to apply wp-let.It breaks up sequential composition  1 ;  2 ≜ let =  1 in  2 by putting the wp of  2 into the post of  1 , turning the goal into wp Next, we apply wp-assign.It imposes an additional side condition on the context (via abd-embed), namely v r should be some location ℓ for which we have a points-to assertion ℓ ↦ → .Thus, leaving: The rule wp-assign has transformed the goal such that we should first give up the ownership of ℓ (with "ℓ ↦ →  * ") and then we get back the updated ownership again (with "ℓ ↦ →  [s := v a ] − * ").We do the former with abd-res-ctx and the latter with abd-wand-res, leaving us to prove We proceed in a similar fashion for the second assignment, updating the points-to assertion to ℓ ↦ →  [s := v a , e := v b ], which satisfies the desired postcondition.
Version 2: Abducting the precondition.We now turn to inference.As before, we deductively verify Δ * [] ⊢ wp  {Φ} in goal-directed fashion-except that now we allow for the possibility that the context Δ was not sufficient to prove the goal, and we infer the missing precondition .This means that, whereas Δ, , and Φ are inputs,  is an output.To stage the presentation, we will make the simplifying assumption in this version that the post Φ is simply True.
To infer the precondition  init , we solve True} where the context is empty.The proof search proceeds as in Version 1 until we reach the first assignment: At this point, the rule wp-assign does not apply anymore, since the ownership of v r is not in the context.(It is empty!)Instead, the missing ownership should come from the precondition that we want to infer, so we must add it to the precondition .To do so, we proceed in several steps.First, we use a second rule for assignment, wp-assign-def, resulting in: Next, since the goal has become an existential quantifier, we apply the rule for existential quantification, abd-exists (twice).It adds existential quantifiers for ℓ and  to the precondition .Subsequently, since loc(v r , ℓ) and ℓ ↦ →  are not contained in the context ∅, they are also added to the precondition -with abd-pure-missing for loc(v r , ℓ) and abd-res-missing for ℓ ↦ → .We are left to derive and have already constructed  ≜ ∃ℓ,  .loc(v r , ℓ) * ℓ ↦ →  *  ′ for some  ′ that is yet to be determined.From here, the derivation proceeds as in Version 1 until we eventually arrive at the post True, facing loc We finish with abd-true, resolving  ′ ≜ True.Thus, we have inferred the pre  = (∃ℓ,  . Version 3: Abducting the postcondition.Next, let us infer the postcondition.Intuitively, the postcondition should be the context at the end of the derivation (i.e., "Δ" in abd-true).The judgment Δ * [] ⊢ wp  {Φ}, however, does not have an output for the postcondition, only for the precondition .The reason is that such a dedicated postcondition output is not needed-rather, we can encode the post as part of the pre by expressing specifications as predicate transformers.
A predicate transformer is a function  from postconditions Φ to preconditions  Φ such that ∀Φ.{ Φ}  {Φ}.Predicate transformers are an alternative to Hoare triples for specifying functions, where we use existential quantification (+ separating conjunctions) to express preconditions, and universal quantification (+ magic wands) to express postconditions.We use colors to highlight the precondition parts (in light blue) and postcondition parts (in violet).For example, In this case, the value v r should be a location ℓ storing a struct with contents  (precondition), and afterwards the location ℓ stores the updated contents  [s := v a , e := v b ] (postcondition).
To infer a predicate transformer specification, we treat the predicate Φ abstractly during the abduction.Then, the resulting precondition (Φ) is a predicate transformer, because Φv} where Φ is abstract.We eventually hit the postcondition Φ ⟨⟩ (in place of "True" in Version 2).At this point, we face loc To finish the derivation, we "revert" the context before the post with abd-end ("∀ì " explained below), resulting in the solution , we see that (Φ) coincides exactly with the specification  init (Φ).Thus, we have inferred the specification { init } init(v r ,v a ,v b ) { . init }-stated as a predicate transformer instead of a Hoare triple.

Existential Quantification
The goal-directed proof search presented above is overly simplistic in a key dimension: its treatment of existential quantification.For existential quantification, there are really two options: (1) lift the quantifier to the precondition (as above) or (2) instantiate the quantifier.For abductive deductive verification to work, we need both options.With init, we have already seen a case where we must lift the existential quantifier to the precondition, because the context ∅ is empty (in §2.1).To illustrate when we want the second option, we consider the function mk_range.
For mk_range, we want to infer the following specification: The precondition is True and the postcondition ensures that the return valuev r is a location ℓ storing a correctly initialized range.To understand why inferring this specification requires existential instantiation, we unfold the weakest precondition semantics of wp mk_range(v a ,v b ) {Φ} because it reveals the existential quantifier that we must instantiate.We write  ⊨  for semantic entailment.Specifically, we unfold mk_range, the let-binding, the allocation new(), and the init-call: Note the existential "∃ℓ,  ." arising from the call to init.We should not lift this quantifier into the precondition, because its value depends on the result of allocating a fresh location inside mk_range.Instead, we should instantiate this quantifier with the location ℓ obtained from new() (and  with ).Instantiating existentials.To instantiate existential quantifiers during goal-directed proof search, we introduce a new goal ex( . ) (semantically ∃ .  ⊨ ex( . )).It is triggered for function calls  (ì v) to instantiate existential quantifiers in the spec of  .That is, for function calls  (ì v), we use wp-call, which looks for a specification  for  (by searching for spec( , )) and then creates the function application goal "apply( ){Φ}" (semantically  Φ ⊨ apply( ){Φ}).The proof search treats apply( ){Φ} roughly as "ex( . Φ)" (see §3) to instantiate existentials in  .
For example, in the inference of mk_range, we eventually encounter the goal: where ex contains the precondition part of  init and we summarize the remainder as " rest ".A simplified selection of rules for ex( . ) is depicted in Fig. 5 as the specification for mk_range.The underlying issue is that while predicate transformers can, in principle, contain arbitrary quantifier alternations, a predicate transformer  that goes beyond a single ∃∀-alternation can barely be considered a specification: it alternates preconditions (∃) with postconditions (∀), thus making it difficult to understand what  means as a specification.Therefore, a key design decision of Quiver is to restrict ourselves to a single ∃∀-alternation.
In fact, this is enforced by an asymmetry in our quantifier rules: abd-exists adds existential quantifiers to the precondition, but abd-all does not add universal quantifiers; it only introduces them in the goal.The rule that adds universal quantifiers to the precondition is abd-end, used only at the very end.It adds those universal quantifiers that have been introduced by abd-all in the goal and are potentially now contained in the context Δ.The key benefit of a single ∃∀-alternation is that, in the resulting predicate transformers, preconditions (∃) always appear before postconditions (∀).

Specification Sketches
So far, we have discussed how we can infer specifications without any user guidance.The resulting specifications describe "low-level" memory footprints, but they do not yet use any abstract predicates (i.e., user defined predicates for data types).Abstract predicates are, however, a hallmark of separation logic verification.For instance, for the range data type, a standard approach would be to conceal the implementation details behind a predicate range(v r ,  s ,  e ), which can be understood abstractly as "v r is the range [ s ,  e )".To guide the inference towards "high-level" specifications with abstract predicates, we integrate specification sketches into abductive deductive verification.

Specification sketches.
To explain what sketches are and how we integrate them, we continue with the range example.We define the abstract predicate range(v r ,  s ,  e ): The predicate ensures that the s-field and e-field are integers  s and  e , and that the integer bounds form a valid, non-negative range by imposing 0 ≤  s ≤  e .
To infer specifications involving the range-predicate, we add sketches to the implementation of the range operations.A sketch is an inline assertion "assert(. ..)", which describes part of the logical state at the program point, and which may use question marks "?" to leave holes in the description.For example, in init, we add the assertion "assert(range(r, ?, ?))" to mean r is some range [?, ?) at the end of the init function.The idea is that the proof search then takes this sketch into account and adjusts the resulting specification.That is, for init, the inferred specification becomes: The precondition is extended by three new assumptions: int(v a ,  a ) and int(v b ,  b ) requiring v a and v b to be integers  a and  b , and 0 ≤  a ≤  b to impose the range constraint on the integers.The postcondition is changed to indicate that v r stores the range [ a ,  b ) after calling the function.
Abducting specification sketches.As far as abductive deductive verification is concerned, every sketch corresponds to a separation logic proposition ∃ . , with existential quantifiers for question marks.For example, "assert(range(r, ?, ?))" in init corresponds to "∃, .range(v r , , )".We use the sketch to update the internal separation logic state at the point of the assertion.Concretely, to integrate sketches into Δ * [] ⊢ , we introduce a new goal assert( . ){ }.When we encounter assert( . ){ }, we first (1) prove   for some -abducting additional preconditions where necessary-and, subsequently, (2) assume   for the remainder of the inference.To deal with the existential "" in the sketch "∃ . ", we define assert( . ){ } using ex, roughly as "ex( .  * (  − * ))" (see §3).Here, we use the same pattern as wp-assign and wp-read: we first produce " " and then assume " " again for the remainder of the inference.
For example, inside the derivation of init, we encounter wp (assert(∃, .range(v r , , ))) {Φ}.We apply (wp-assert) and are confronted with the new assert-goal: It boils down to ex(, .range(v r , , ) * (range(v r , , ) − * Φ ⟨⟩)).The first part, "range(v r , , ) * ", is handled by (a) unfolding range(v r , , ) with ex-unfold and, then, (b) abducting anything missing for proving the body of range(v r , , )-here int(v a ,  a ) int(v b ,  b ), and 0 ≤  a ≤  b -while also instantiating  ≜  a and  ≜  b .The second part, "range(v r , , ) − * ", adds range(v r ,  a ,  b ) to the context (abd-wand-res), which then eventually ends up in the postcondition of  ran init (via abd-end).Sketches vs. specifications.The other two range operations highlight two important benefits of specification sketches over full-fledged specifications.First, we can provide similar sketches for multiple functions, yet obtain different specifications.For example, for the function size, we provide the same assertion sketches as for init, yet obtain:  ran size (Φ) ≜ ∃ s ,  e .range(v r ,  s ,  e ) * (∀ .range(v r ,  s ,  e ) * int(,  e −  s ) − * Φ ) The precondition range(v r ,  s ,  e ) arises, because unlike for init, when we encounter the first sketch in size, the context contains no information about v r that could be used to prove range(v r ,  s ,  e ).

State Proposition
Fig. 6.The syntax of Argon.Thus, the resource is added as a whole to the precondition (via ex-pred and then abd-res-missing).
The postcondition contains the additional information that the return value  is the integer  s −  e .Second, abductive deductive verification is compositional.The sketch in init not only affects init, but also mk_range.That is, if we infer a specification of mk_range again-against the new specification  ran init -we obtain the following specification without any additional sketches: The precondition changes to incorporate the additional assumptions on v a and v b , and the postcondition ensures that the return value v r is the correctly initialized range(v r ,  a ,  b ) from init.The context Δ consists of three parts: pure assertions Γ (e.g.,  ≥ 0 and loc(v, ℓ)), ownership assertions Ω * , and persistent assertions Ω □ .The assertions in Ω * and Ω □ are resources , the basic building blocks to describe the program state (e.g., ℓ ↦ →  and range(v, , ) in §2).The persistent resources in Ω □ remain in the context forever while the ownership resources in Ω * can be removed.

THE ABDUCTION ENGINE ARGON
Goal-directed proof search.The key technique that turns Argon's inference rules into an automated abduction algorithm is goal-directed proof search: At each step of the abduction with state Δ * [] ⊢ , Argon matches on the goal  and applies the first rule with a matching conclusion and whose side conditions can be proven (where the order in the paper is left-to-right, top-to-bottom).After applying the rule, it then recursively proceeds with the premises.The goals that Argon supports are depicted in Fig. 6.Below, we discuss which purpose they serve, and how they are dealt with during goal-directed proof search, using the rules in Fig. 4 and Fig. 7.
Embedded goals.Embedded goals  sit at the heart of Argon.They embed deductive proof systems into Argon such as a weakest precondition calculus (in §2) and the type system Thorium (in §4)-using an extensible set of reasoning rules.Concretely, when Argon encounters an embedded goal  (abd-embed in Fig. 4), it searches for a reasoning rule  ⊣  when  by matching on  and continues with the goal  if the side condition  is provable in the current context Δ. Embedded goals can be weakest preconditions wp  {Φ}, but also other auxiliary judgments (e.g., Thorium introduces a judgment for "type conversion", discussed in the appendix [56]).
Separating conjunction and magic wand.We turn to separating conjunction  *  and the magic wand  − * .The goal  *  instructs Argon to prove the assertion  while  − *  introduces  into the context.Argon avoids ambiguity during the proof search by restricting  to state propositions, i.e., assertions over the state of the program consisting of resources and pure assertions.(For efficient proof automation, Sammler et al. [52] employ a similar restriction, but not in the context of abduction.)We consider the two interesting cases: If  =  is a pure assertion, we either prove  through a pure entailment Δ ⊢  (abd-pure-prove in Fig. 4), or we add  to the precondition and the context (abd-pure-missing in Fig. 4).If  =  is a resource, we either find the resource  in the context (abd-res-ctx in Fig. 4), or we add it as a missing assertion to the precondition (abd-res-missing in Fig. 4).The other cases for  (namely,  1 *  2 and ∃ . ′ ) are handled by straightforward rules (not shown) which serve to hoist out existential quantifiers and move a  or  to the left side of the goal using associativity of separating conjunction.We adopt a similar restriction for magic wands  − * .It lets us split  into pure assertions  and resources  that are then added to the context (abd-wand-pure resp.abd-wand-res in Fig. 4).

Conditionals and conjunctions.
We turn to conditionals if  then  1 else  2 and conjunctions  1 ∧  2 .For a conditional if  then  1 else  2 , we first try to eliminate it by proving or disproving the condition  (i.e., by applying abd-if-true or the corresponding rule for Δ ⊢ ¬).Otherwise, we lift it to the precondition (abd-if). 2For conjunctions  1 ∧  2 , we lift the conjunction to the precondition (abd-conj).Conceptually, a conjunction means the reason for the choice between both branches is internal to the function, i.e., it cannot be influenced by the caller.For example, a call to malloc may return NULL or a valid pointer.A client cannot influence which one it is, making the predicate transformer for malloc a conjunction of both cases (shown in the appendix [56]).
Quantifiers and post conditions.Existential quantification ∃ . in the goal is resolved by adding an existential quantifier to the precondition  (abd-exists in Fig. 4) and universal quantification ∀ . is resolved by introducing  in the goal but leaving the precondition  unchanged (abd-all in Fig. 4).Universal quantifiers are added to the precondition only when we reach the postcondition goal Φv (abd-end in Fig. 4).Concretely, when we encounter Φv, we revert those ì  that have been previously introduced in the goal (with abd-all).They are added in front of the context Δ since the context might refer to them at this point (e.g., v r and ℓ in  mk_range in §2.2).Simplification.One of the main ways 3 to integrate reasoning about mathematical theories into the Argon proof search is simplification.It comes in two forms: (1) a general-purpose simplification judgment  ⇒  (semantically  ⊨ ), which simplifies  into  and (2) a goal simpl( ){ ′ .  ′ } that uses the judgment  ⇒  to simplify a predicate transformer  (abd-simpl).The simplification judgment  ⇒  proceeds in three steps 4 (simplify),  1 ⇒ norm  2 ⇒ simp  3 ⇒ ex  4 : First, with  1 ⇒ norm  2 , we normalize  1 into a normal form analogous to predicate transformers in Fig. 6 (e.g., by lifting out existentials).Then, with  2 ⇒ simp  3 , we simplify pure propositions in  2 .Finally, with  3 ⇒ ex  4 , we existentials based on equalities  =  in  3 .For example, We use simplification rules and solvers for, e.g., integers, injective functions, and lists, and the simplification rules can be extended as needed.
Existential instantiation.The goal ex( . ) is used for existential instantiation.It has the following key characteristics: it is agnostic to the order of existential quantifiers in ; it is agnostic to the order of conjuncts in a separating conjunction; it inherits the simplification of  ⇒ ; and it allows us to destruct existential quantifiers in the context.Moreover, similar to embedded goals , it is extensible in the sense that additional rules can be added.To achieve these characteristics, we generalize ex( . ) to the form "ex( .  |  )", where the state proposition  collects "blocked" assertions (explained below) and define ex( . ) ≜ ex( .  | True).
The proof search for ex( .  |  ) proceeds by applying existential instantiation rules of the form ex( . 1  |  ) ⊣  2 when  (abd-ex), analogous to the rule for embedded goals (abd-embed in Fig. 4).We discuss the most important rules, depicted in Fig. 8 (and omit rules such as applying associativity for separating conjunction): For existential quantifiers ∃. , we add a binding (exexists).For pure propositions  that do not depend on , we lift them out of the goal (ex-pure).For pure propositions  that make  precise (e.g., equality), we use simplification to instantiate the existential (ex-eq).For pure propositions that depend on  but do not lead to instantiation, we put them on the "blocked stack", meaning we add them to the state goal  (ex-pure-blocked).The blocked stack allows us to traverse further into the goal  and, thereby, we can be agnostic about the order of conjuncts in a separating conjunction.A blocked assertion may become unblocked when we can instantiate an existential.Thus, ex-eq adds the "blocked stack" back into the goal.
For resources , each instantiation of Argon can handle them as desired by extending the rules of ex( . 1  |  ) ⊣  2 when .For example, we have seen (simplified) rules in Fig. 5, and Thorium adds rules for its resources: type assignments (see §4).Finally, we have rules for when the goal  is stuck in the sense that no other rule applies: if there are existentials left to instantiate (ex-lift), we lift one of them outside and, once there are none left (ex-done), we continue with the goal .
Sequential composition.The goal bind(Φ. 1 Φ){ . 2  } implements sequential composition of abduction goals (abd-bind).It works as follows: First, it will abduct  1 for an arbitrary postcondition Φ.The result of this abduction is a predicate transformer  .Then, it will abduct  2 , passing it the newly abducted predicate transformer  as an argument.(To implement sequential composition, abd-bind has to avoid duplicating ownership, and therefore gives only the persistent part of the context Δ □ to  2 .)In other words, bind makes abduction available inside of an abduction.
With bind, we finally have all the pieces needed to define the goals for applying predicate transformers apply( ){Φ} (from §2.2) and specification sketches assert( . ){Φ} (from §2.Both goals have a similar structure.For apply, we instantiate the existentials in  using ex, and for assert, we instantiate the existentials in the sketch " " using ex.We wrap the instantiation of existentials in the sequential composition bind to "cleanup" after ex with a simplification simpl.That is, since the rules for existential instantiation are extensible (abd-ex), they can trigger arbitrary auxiliary goals, which may (indirectly) add existential quantifiers to the precondition .
By simplifying afterwards, we can potentially eliminate some of these quantifiers (e.g., one goal might add "∃.• • • " and another might add " = 0", which is then simplified by picking  ≜ 0).
Loops.Argon does not infer loop invariants, but supports loops with manually provided loop invariants (without sketches).For a given loop invariant  inv , the proof search proceeds in four steps: (1) when we reach the loop, we abduct the invariant  inv using ex; (2) we abduct the body of the loop assuming the loop invariant  inv ; (3) before the next iteration, we reestablish  inv again using ex; finally, (4) we check that  inv is indeed a loop invariant by ensuring the abduction of the loop body did not require any additional preconditions.
Failure.Finally, if no other Argon rule applies, the inference fails (abd-fail).In this case, Argon terminates by inserting a marker into the precondition and provides the partial inferred precondition to the user.In impossible cases (e.g., a location is NULL, but we are supposed to provide ownership), the marker can contain information explaining what went wrong, provided by the Argon instantiation.To remain sound, the marker is semantically interpreted as False, as indicated in abd-fail.

THE TYPE SYSTEM THORIUM
In §2, we have seen how to apply abductive deductive verification to a simple separation logic.In Quiver, to scale to the complexities of C, we use a richer separation logic, Thorium.Following in the footsteps of RefinedC [52], Thorium is a separation logic-based type system.In the following, we explain its core ingredients: type assignments and typed weakest preconditions.A more detailed discussion of how Thorium builds proof search on top of these core ingredients-using typing rules in place of the weakest precondition rules in Fig. 4-is contained in the accompanying appendix [56].
Type assignments.Instead of abstract predicates P(v, ) and points-to assertions ℓ ↦ → v (as we considered in §2), resources in Thorium are type assignments.They are of the form v ⊳   (read "v is an ") and ℓ ⊳   (read "ℓ stores an "; semantically (∃v. For each type , they have an interpretation in terms of more traditional separation logic assertions.For example, the resource v ⊳  own ℓ (num[int] ) means "v is an owned pointer ℓ storing the int-integer ", which formally boils down to v The types of Thorium are depicted in Fig. 9.We explain the most important types by returning to the range data type ( §2).In C, it would be declared as typedef struct ran {int s; int e} *range; and, for the predicate range(v,  s ,  e ) (from §2.3), the analogous Thorium type is defined as: ( s ,  e ) @ range ≡ ty ∃ℓ.Types of the form ì  @ P correspond to user-defined abstract predicates and are defined via a (possibly recursive) equation ì  @ P ≡ ty .The type ( s ,  e ) @ range ensures that its values are owned pointers ℓ (via "own ℓ ") to a ran-struct (via "struct[s] ì ") with two fields: s containing int-integer  s (via "num[it] "), and e containing the int-integer  e .To hide the location ℓ, we use type-level existential quantification "∃ . " and, to impose the bounds constraint 0 ≤  s ≤  e , we use type-level separating conjunction " * ".Besides the bounds constraint, the type carries an additional constraint: the resource "block ℓ ".It tracks the length of dynamically allocated blocks (e.g., via malloc; see Fig. 1) to ensure that ownership of the entire block is given up when freeing ℓ.
Typed weakest preconditions.Instead of standard weakest preconditions wp  {v.Φv} (in §2), we use typed weakest preconditions wp  {v, .Φv } in Thorium: their postcondition Φ is about the resulting value v and, additionally, its type .This type  can then be used by auxiliary embedded goals like cast (it 2 ) it 1 (v : ){Φ} (for C-level integer type cast) to steer the proof search-via typing rules in place of the weakest precondition rules in Fig. 4. For example, to cast an int into a size_t integer, the typing rule cast (size_t) int (v : num[int] ){Φ} ⊣  ≥ 0 * ∀ .Φ  (num[size_t] ) requires  to be non-negative and continues with  as a size_t-integer in the postcondition.We expand on how types and typing rules affect the proof search in the accompanying appendix [56].

IMPLEMENTATION
We have developed a prototype implementation of Quiver in Coq.More specifically, we have implemented the goal-directed abduction engine Argon Δ * [] ⊢  (which embeds the typing rules of Thorium) as an automated abduction procedure in Coq.For a given C function (and possibly a sketch), it (1) infers a specification and, at the same time, (2) proves its correctness.
We use the Coq proof assistant as a foundation for Quiver for two main reasons: First, Quiver inherits Coq's rich logic for expressing complex correctness properties (as evaluated in §6).Second, it allows us to ensure the correctness of the inferred specifications.Concretely, we have proven Quiver's inference foundationally sound against RefinedC's C semantics, Caesium.Caesium provides a detailed formalization of C, modeling many challenging features ranging from bounded integers and pointer arithmetic, over uninitialized memory with poison semantics and address-of operator (also on local variables), to manipulation of the underlying byte-level representation of 1 vec_t mkvec(int n){ size_t s=sizeof(int)*(size_t)n; vec_t vec=xmalloc(sizeof(*vec)); 2 vec->data=xzalloc(s); vec->len=n; [[q::type(?@ vec_t)]] return vec; } values. 5To prove Quiver sound against Caesium, we have used the separation logic framework Iris [29,28,30] to model Argon and Thorium.We have proven all rules sound against this model: Theorem 5.1.All Argon and Thorium rules are sound wrt. the Caesium C semantics.
The automated abduction procedure combines the soundness of the individual rules into a foundational proof that the inferred specifications are sound.In our examples, we assume specifications for common operations from the C standard library (e.g., malloc, memset, and abort in Fig. 1), which can be found in the accompanying appendix [56].Thus, Corollary 5.2.Assuming the standard library function satisfy their specifications, the specifications inferred by Quiver are sound wrt. the Caesium C semantics.
Finally, Quiver comes with a frontend that automatically translates annotated C code into (1) corresponding Caesium code, (2) type declarations in Thorium, and (3) calls to the abduction procedure for Argon.The abduction procedure is implemented using Coq's Ltac tactic language [16] and typeclass mechanism [55].

EVALUATION
To evaluate Quiver, we have applied it to several interesting case studies, listed in Fig. 12.We split our evaluation into two parts: First, we take a closer look at a specific case study, a vector, to get a sense of the kind of specifications that Quiver can infer (in §6.1).Then, we discuss the aggregate results of evaluating Quiver on these case studies (in §6.2).

The Vector Case Study
Inspired by C++ and Rust, a vector is a dynamically-sized array that tracks its length.An excerpt of the vector implementation is depicted in Fig. 10.In this implementation, vectors are of C type typedef struct vector {int *data; int len;} *vec_t.They are pointers to a struct with two fields: the data-field storing the contents of the vector in a dynamically allocated array of integers and the len-field tracking the length of the vector.For vectors in Quiver, we define the Thorium-data type:  That is, for a mathematical list of integers xs, a value of type xs @ vec_t is an owned pointer ℓ to a vector-struct.It stores in its data-field an owned pointer r to an array of integers xs and in its len-field the length of xs as an integer.It tracks the memory block resources of ℓ of size sz vec and r of size sz int • len xs where sz vec ≜ sizeof(struct vector) and sz int ≜ sizeof(int).We focus on two vector operations (see the appendix [56] for more): The operation mkvec creates a new vector of length  initialized with zeros, and the operation vec_grow extends a vector by allocating a new underlying buffer.Concretely, vec_grow allocates a new content array buf of larger size (Line 8), copies the contents of the old array over (Line 9), frees the old array (Line 10), sets all uninitialized memory to zero (Line 11), and returns the new length (Line 12).
Sketches and inferred specifications.For each operation, the specification sketches are annotated with "[[q::...]]" in Fig. 10, where q::requires sketches preconditions, q::ensures postconditions, q::type the type of an expression, and ˆvec means v vec .The inferred specifications are depicted in Fig. 11.For mkvec, Quiver infers that the size  must be a non-negative int-integer and that the return value is a vec_t-vector filled with 0  , a list of  zeros.For vec_grow, Quiver infers a conditional specification: if  ≤ len xs, the vector is unchanged and len xs is returned; otherwise, the vector grows by  − len xs zeros and  is returned as the new length.To arrive at this specification, Quiver (1) infers the type of the unspecified argument v new , (2) resolves the quantifier alternations that arise from each memory operation (a ∃∀ for each operation), (3) instantiates the sketches (including xs + + 0 −len xs @ vec_t for the second case), (4) proves that len(xs + + 0 −len xs ) =  when  > len xs, and (5) prunes the branch returning 0 using the fact that xs @ vec_t is never NULL.
Abductive deductive verification.The vector case study illustrates concisely the benefits of abductive deductive verification.On the one hand, we are doing expressive separation logic verification.For example, (a) vectors track their contents as a mathematical list of integers, (b) vectors maintain the invariant that the length of the list is stored in the field len, (c) dynamically allocated memory can be of variable length, which is tracked via a predicate block, (d) pointer arithmetic is used to compute fields of structs and members of arrays, and (e) pointer-level operations (e.g., memset and memcpy) are used to manipulate high-level data types (e.g., arrays).On the other hand, we can significantly benefit from inference for the verification.In particular, we only need to provide the key bit of information-that a certain value is a vector-and can use inference to complete the rest.In the accompanying appendix [56], we show that the same sketches suffice for additional, quite different vector operations (e.g., for getting and setting elements in the vector).

Aggregate Evaluation
We evaluate the prototype implementation of Quiver on three axes: (1) the expressivity (compared to bi-abduction), (2) the specification overhead (compared to RefinedC), and (3) the merit of the inferred specifications.We do so using the case studies in Fig. 12.For each case study, we provide a more detailed discussion in the appendix and all implementations and inferred specifications can be found in the Coq development [56].The Allocators case study considers common wrappers around standard library functions for memory allocation (e.g., xmalloc and xzalloc).The Linked List case study considers a singly linked-list implementation with pointer elements, and the Vector case study extends the vector from §6.1.The OpenSSL Buffer and Bipbuffer case studies consider open-source buffer implementations from OpenSSL [42] and memcached [37].The Binary Search case study considers binary search on sorted integer lists, and the Hashmap case study considers a hashmap with linear probing.For each case study, we measure the execution time on a single core of an Apple M1 Pro processor (Time).
Expressivity (vs.Bi-abduction).To understand the degree of expressivity that Quiver supports, we consider several types of specifications (Type in Fig. 12), increasing in complexity: We infer memory safety specifications (mem) for several examples-including the Allocators, whose inferred specifications (e.g., xmalloc and xzalloc) we use in other case studies.We infer length specifications (len) for the open-source buffers, which track the length of the buffer and data type invariants about its fields.We infer functional specifications (func) for the Linked List and the Vector, which track their contents as mathematical lists.And, to test the boundaries of Quiver, we consider a binary search implementation and a Hashmap, a version of the most complex functional correctness case study of Sammler et al. [52] specialized to integer values. 6he case studies demonstrate that Quiver, embedded into Coq, supports expressive separation logic reasoning over a variety of mathematical domains (e.g., integers, lists, maps, and custom inductive types).For example, Quiver figures out that (a) if  < 0x5ffffffc, then ( + 3)/3 • 4 will not overflow the size_t type (OpenSSL Buffer) and (b) grow results in the vector xs + + 0 −len  , which extends the original list xs with  − len  zeros (Vector).Moreover, provided with loop invariants and additional Coq lemmas and definitions, Quiver does significant functional correctness reasoning for the Binary Search and Hashmap.The expressivity of Quiver goes considerably beyond the original bi-abduction inference [5,6] and also what is nowadays used in Infer [26]. 7In exchange, it requires more input from the user, in particular for more expressive specifications.
Specification overhead (vs.RefinedC).To understand how much specification Quiver infers, we compare the size of the inferred specifications (Specs) with the size of our sketches (Sketch) and other annotations (Annot).We measure the size of specifications and sketches by counting the number of quantifiers, conditionals, conjunctions, type assignments, and other individual preand postconditions (e.g., the size of  mkvec would be 5).We separately count other annotations such as type definitions, loop invariants, and inference instructions.A handcrafted specificationas it would be provided in RefinedC-could in some cases reduce the size (e.g., by joining the branches in  grow ), but nevertheless comparing sketches and specs gives an idea how much Quiver infers.Concretely, for the "memory" case studies, we provide no sketches-the specifications are completely inferred.By design, they are low-level (e.g., see Fig. 1) and can be verbose.For all other case studies, we provide sketches.They are typically significantly smaller than the resulting specification and often contain ?-holes (e.g., all 14 Vector sketches boil down to ?@ vec_t).In RefinedC, by contrast, specifications have to be provided in full.Among our case studies, there are two outliers: the Binary Search and the Hashmap.This is no surprise, since both require nuanced, ad-hoc functional correctness reasoning with additional pure Coq definitions and lemmas (Coq).For them, the specification overhead is overshadowed by the additional proof overhead.Nevertheless, even for those two, Quiver does interesting inference: it completes the return type of Hashmap init, and it derives the postcondition of the Binary Search from a loop invariant.
Merit of the specifications.The specifications that Quiver infers provide four key benefits: First, they are an additional form of documentation.Quiver outputs a pretty-printed version of the inferred predicate transformer, which can be read by humans.For example, in the Vector, Quiver adds the constraints on the vector size in the specification of mkvec.Second, the inferred specifications provide assurances about the code.That is, due to soundness (Corollary 5.2), the inferred specifications cannot "hide" any preconditions that are undocumented in the code.For example, in the Bipbuffer, Quiver discovers a fact about the implementation that is easy to miss in the code: the implementation uses mismatched integer types (e.g., the size field of the buffer uses unsigned long int, but the corresponding accessor function returns int), resulting in an additional precondition in the generated specifications.Third, the inferred specifications are compositional.We inherit compositionality from working in separation logic.In particular, in many of the case studies, we infer specifications of auxiliary functions, which are then reused in the inference of others (e.g., BipBuffer, OpenSSL Buffer, and Hashmap); and we use the inferred Allocator specifications in other case studies (e.g., in the Vector, List, Hashmap).Fourth, the inferred specifications abstract over the implementation.By insisting on a single ∃∀-alternation, Quiver ensures that the inferred specification condenses the implementation into preconditions and postconditions.In doing so, it takes care of the intricacies of the C implementation and intermediate proof obligations.To gain some insight into how much work goes into this summarization, we count the number of instantiated existentials (∃) and proven/simplified side conditions ().
Real-world code.Finally, our case studies test whether Quiver can handle the complexities of real-world code.We have applied Quiver to two buffer implementations taken from popular open source libraries, OpenSSL [42] and memcached [37].For the OpenSSL buffer, we track the length and capacity of the buffer and enforce an invariant that the buffer capacity is always larger than 7 For example, Infer does not do integer reasoning such as (a) if  < 0x5ffffffc, then ( + 3)/3 • 4 ≤ 0xffffffff from the OpenSSL buffer or (b) after the loop int k = 0; while (k < 10) k++ the counter  is 10.Quiver automatically proves the former without any guidance, and infers the latter when guided with the loop invariant  ≤ 10. the contents.For the memcached buffer, a bipartite buffer, we track the length and the relationship between the fields that track the segments of the buffer.

RELATED WORK
In the literature on separation logic verification, there is a wide gap between approaches for (a) automatically inferring specifications vs.(b) verifying functional correctness in rich separation logics.In the first camp, there are approaches such as bi-abduction [5,6], which fix a particular fragment of separation logic, and then carefully design automation to infer specifications in it.This line of work started out with shape specifications (i.e., linked list segments and points-to assertions) and, over the years, edged closer toward functional properties by extending the base domain to include constraints on integers, arrays, or bags.In the second camp, there are approaches such as RefinedC [52], which are designed for proving full functional correctness in rich separation logics, as supported by the verification frameworks in which they are embedded (e.g., Coq [13] and Iris [29]).This line of work, over the years, developed increasingly strong proof automation but left specification inference largely untouched.
Quiver sits right in between these two camps, supporting a wide range in between automated and expressive specifications (see §6.2).Typically, Quiver requires more specification guidance from users than a fully automatic inference (increasing with expressiveness of the specification), but significantly less than traditional, deductive approaches for rich separation logics.In exchange, it does not fix any particular mathematical domain and, instead, is implemented in a general-purpose proof assistant-producing certifiably correct specifications.We first compare closely with work in both camps, and then branch out to other related work.
The key contribution of these extensions is to automate the inference over their respective domain.
In contrast, Quiver's specification inference is fundamentally different.By using abductive deductive verification, Quiver is less automated but, in exchange, handles a much richer separation logic by building on existing approaches for deductive proof automation.For example, the vector example ( §6.1)-combining low-level pointer operations, arrays, and integer arithmetic-goes beyond all of these extensions, especially considering the detailed C semantics it is verified against.
Outside of the context of bi-abduction, Dohrau et al. [19] use a static analysis to infer access permissions for array-manipulating programs, and Ferrara and Müller [20] show how to automatically infer access permissions using abstract interpretation.They handle different permission models and loop invariant inference but do not consider functional correctness properties.
Functional correctness verification using separation logic.There is a wide range of approaches for verifying functional correctness based on separation logic [27,2,7,39,52,45], most of which do not infer specifications.We compare to the most closely related work and approaches with some form of specification inference.
A key inspiration for Quiver is RefinedC [52], which provides automated and foundational verification of C code.Its approach of using a type system embedded in separation logic served as a direct inspiration for Quiver.However, RefinedC does not infer specifications and, hence, relies on user-provided, complete specifications.To tackle specification inference, we introduced the abductive deductive verification approach, implemented a proof engine for abduction-Argon-from scratch, and designed a type system-Thorium-that integrates seamlessly with abduction.
For VeriFast [27], a separation logic-based functional correctness verifier for C and Java, Vogels et al. [59] implement a bi-abduction-based shape analysis.Unlike Quiver, it does not infer functional correctness specifications and only infers a postcondition from a user-provided precondition.Separately, Automated VeriFast [38] leverages errors reported by VeriFast to extend user-written specs with additional pre-and postconditions.Automated VeriFast has only been demonstrated on predicates tracking the length of singly-linked lists.
Dohrau [18] presents a learning-based permission inference for the Viper automated verifier [39].Their approach can automatically infer loop invariants and predicate definitions, but only considers permissions, not functional correctness properties.
Liquid types.Liquid types [50,51,58,33] provide a refinement type-based approach for lightweight verification.Liquid types focus on the inference of pure refinements, not separation logic ownership, and often consider more shape-like properties than Quiver.For example, Lehmann et al. [33] describe a vector similar to vec_t from §2, but only track the length in the refinements, not its precise contents.In exchange, liquid types are more automated: they infer refinements and, additionally, loop invariants automatically.
Specification inference for other logics.Outside of the context of separation logic, a separate body of research [54,1,44] considers inferring specifications for programs that do not involve pointer manipulation or a heap.This restriction sidesteps the main challenges this paper focuses on (see, e.g., the vector in §6.1).In exchange, they typically obtain exact (i.e., sufficient and necessary) preconditions, whereas Quiver infers sufficient preconditions.Characteristic formulae.A characteristic formula [10,11] is a direct translation of a program into a separation logic formula.Characteristic formulae are not intended as specifications, but as an intermediate representation used during verification.In particular, they still contain all intermediate proof obligations required to verify a function.In contrast, Quiver infers specifications that summarize the behavior of a function in terms of pre-and postconditions (i.e., in ∃∀-form; see §2.2) by resolving quantifier dependencies and solving side conditions.

CONCLUSION AND FUTURE WORK
With Quiver, we have introduced the idea of abductive deductive verification ( §2) and applied it to specification inference of C programs, using the abduction engine Argon ( §3) and the separation logic based type system Thorium ( §4).In the future, it would be interesting to apply abductive deductive verification, and in particular Argon, to other languages and flavors of separation logic.
Moreover, it would be interesting to investigate loop invariant inference.Finding loop invariants in separation logic is a non-trivial task: it requires finding pure invariants and, additionally, invariants about resources.For restricted fragments of separation logic, loop invariant inference techniques have been developed [35,5,23,25].But for rich separation logics like the one targeted by Quiver, no loop invariant inference algorithms are known.Thus, like Quiver, deductive verification tools for expressive separation logics (e.g., VeriFast, CN, Viper, and RefinedC) require user-provided loop invariants.It would be interesting to investigate how to integrate (a) existing loop invariant inference algorithms for separation logic when the invariant falls into a supported fragment, (b) learning techniques as an approach to loop invariant inference, or (c) existing non-separation logic loop invariant inference techniques by requiring loop invariant sketches (for the resources) but leaving holes for the pure invariants-potentially using abduction to determine how the pure values evolve inside the loop.

Fig. 4 .
Fig. 4. Weakest precondition rules for  expo and generic abduction rules.Overlapping weakest precondition rules are applied top-to-bottom, and overlapping abduction rules are applied left-to-right.
Normalization lifts the existential to the outside, then simplification removes the multiplication with 4 (since 4 = 4 iff  =), and finally instantiation resolves  ≜  based on the equality.As illustrated above, the simplification step  3 ⇒ simp  4 integrates mathematical theories.It uses (a) pure abduction rules  ⇒ simp  , (b) rewriting simplification rules  [] ⇒ simp  [] if  = , and (c) solvers  ⇒ simp True if .