Verified Software Units

Modularity - the partitioning of software into units of functionality that interact with each other via interfaces - has been the mainstay of software development for half a century. In case of the C language, the main mechanism for modularity is the compilation unit / header file abstraction. This paper complements programmatic modularity for C with modularity idioms for specification and verification in the context of Verifiable C, an expressive separation logic for CompCert Clight. Technical innovations include (i) abstract predicate declarations – existential packages that combine Parkinson & Bierman’s abstract predicates with their client-visible reasoning principles; (ii) residual predicates, which help enforcing data abstraction in callback-rich code; and (iii) an application to pure (Smalltalk-style) objects that connects code verification to model-level reasoning about features such as subtyping, self, inheritance, and late binding. We introduce our techniques using concrete example modules that have all been verified using the Coq proof assistant and combine to fully linked verified programs using a novel, abstraction-respecting component composition rule for Verifiable C.


Introduction
Separation logic [61,53] constitutes a powerful framework for verifying functional correctness of imperative programs. Foundational implementations in interactive proof assistants such as Coq exploit the expressiveness of modern type theory to construct semantic models that feature higher-order impredicative quantification, step-indexing, and advanced notions of ghost state [4,36]. On the basis of proof rules that are justified w.r.t. the operational semantics of the programming language in question, these systems perform symbolic execution and employ multiple layers of tactical or computational proof automation to assist the engineer in the construction of concrete verification scripts. Perhaps most importantly, these implementations integrate software verification and model-level validation, by embedding assertions shallowly in the proof assistant's ambient logic; this permits specifications to refer to executable model programs or domain-specific constructions that are then amenable to code-independent analysis in Coq.
To realize the potential of separation logic, such implementations must be provided for mainstream languages and compatible with modern software engineering principles and programming styles. This paper addresses this challenge for Verifiable C, the program logic of the Verified Software Toolchain (VST [4]). We advance Verifiable C's methodology as follows.
1. We provide general infrastructure for modular verification of modular programs by extending Beringer and Appel's recent theory of function specification subsumption and intersection specifications [15] to a formal calculus for composing verified software units (VSUs) at their specification interface. Each VSU equips a compilation unit's header file with VST specifications of its API-exposed functions. Composition of VSUs matches the respective import and export interfaces, applying subsumption as necessary. Crucially, a compilation unit's private functions remain hidden and only need to be specified locally. Composition is compatible with source-level linking for Comp-Cert Clight and supports repeated import of library modules ( §3). 2. Utilizing existential abstraction [46] and parametricity, we extend work on abstract predicates [56] to provide clients with specification interfaces that differ in the degree to which module-internal representation details are revealed. This flexibility is achieved by codifying how the reasoning principles associated with a predicate can be selectively communicated to clients, using a device we call (existentially) abstract predicate declarations (APDs) ( §4). 3. To investigate specification modularity in the presence of callbacks, we study variants of the subject-observer design pattern; we demonstrate that by complementing a module's primary predicate with residual predicates, representation hiding can be respected even at transient interaction points, where an invocation of a module's operation is interrupted, the module's invariant may be violated, and yet its internal state must remain unmodified and unexposed until the operation is resumed ( §5). 4. We present a novel approach to foundational reasoning about object principles that modularly separates C code verification from model-level behavior. Exploiting the theory of positive subtyping [30], we cover subtyping, interfaces with multiple implementations, dynamic dispatch, self, and late binding, for a simple Smalltalk-style object model with static typing ( §6). This paper is accompanied by a development in Coq [14] that conservatively extends VST with the VSU infrastructure and contains several case studies. In addition to the examples detailed in the paper, the Coq code treats (i) the running example ("piles") of Beringer and Appel's development [15]; we retain their ability to substitute representation-altering but specification-preserving implementations; (ii) a variant of Barnett and Naumann's Master-Clock example [12], as another example of tightly coupled program units; and (iii) an implementation of the Composite design pattern, obtained by transcribing a development from the Verifast code base [35]. In addition, a VSU interface that unifies the APIs of B + -trees and tries was recently developed by Kravchuk-Kirilyuk [40].
To see how APDs build on Parkinson and Bierman's work, consider a concrete representation predicate in the style of Reynolds [61]: list x α p specifies that address p represents a monotone list α of numbers greater than x: Being defined in terms of →, this definition assumes a specific data layout (a twofield struct). Representation-specific predicates enable verification of concrete implementations of operations such as reverse. But a client-facing specification of the entire list module should only expose the predicate in its folded forma simple case of an abstract predicate. Indeed, while VST fully supports API exposure of structs (incl. stack allocation), all examples in this paper employ an essentially "dataless" programming discipline [8,60,37] in which structs are at most exposed as forward declarations. Clearly, such programmatic encapsulation should not be compromised through the use of concrete predicate definitions.
To regulate whether a predicate is available in its abstract or unfolded form at a particular program point, Parkinson and Bierman employ a notion of scope: predicates are available in their unfolded form when in scope and are treated symbolically elsewhere. This separation can naturally align with the partitioning into compilation units, but is all-or-nothing. But even in the absence of specifications, different clients need different interfaces: C developments routinely provide multiple header files for a single code unit, differing in the amount to which representational information is exposed. Mundane examples include special-purpose interfaces for internal performance monitoring or debugging. Extending this observation to specifications means supporting multiple public invariants. Indeed, several levels of visibility are already conceivable for our simple list predicate: no (un)folding, no exposed reasoning principles: properties that follow from the predicate's definition cannot be exploited during client-side verification; -no (un)folding, but reasoning principles are selectively exposed; for example, one may expose the model-level property that α is strictly increasing, or the fact that the head pointer is null exactly if α is empty; -the set of exposed reasoning principles includes fold/unfold lemmas (perhaps with the least-fixed-point property inherent in the inductive definition of the predicate), but the internal representation of nodes is encapsulated using a further predicate; hence, implementations are free to select a different struct layout, for example by swapping the order of fields; -the predicate definition is fully exposed, including the internal data layout.
APDs support such flexibility by combining zero or more abstract predicate declarations (no definitions, to maintain implementation-independence) with axioms that selectively expose the predicates' reasoning principles. In parallel to programmatic forward declarations, an APD is exported in the specification interface of an API and is substantiated -in implementation-dependent fashion -in the VST proof of the corresponding compilation unit. This substantiation includes the validation of the exposed axioms. When specifying the API of a module, the engineer may not only refer to any APDs introduced by the module in question, but may also assume APDs for data structures provided by other modules (whose header files are typically #included in the API in question). Matching the APD assumptions and provisions of different modules occurs naturally during the application of our component linking rule, ensuring that fully linked programs contain no unresolved APD assumptions.
Before going into technical details, we first summarize key aspects of VST.

Program verification using VST
Verification using VST happens exclusively inside the Coq proof environment, and operates directly on abstract syntax trees of CompCert Clight. Typically, these ASTs result from feeding a C source file through CompCert's frontend, clightgen, but they may also originate from code synthesis. Either way, verification applies to the same code that is then manipulated by CompCert's optimization and backend phases. This eliminates the assurance gap that emerges when a compiler's (intermediate) representation diverges syntactically or semantically from a verification tool's representation. The absence of such gaps is the gist of VST's machine-checked soundness proof: verified programs are safe w.r.t. the operational semantics of Clight; this guarantee includes memory safety (absence of null-pointer dereferences, out-of-bounds array accesses, use-after-frees,. . . ) but also absence of unintended numeric overflows or race conditions. As Clight code is still legal C code (although slightly simplified, and with evaluation order determinized), verification happens at a level the programmer can easily grasp. In contrast to other verification tools, VST does not require source code to be annotated with specifications. Instead, the verification engineer writes specifications in a separate Coq file. By not mixing specifications (let alone aspects of proof, such as loop invariants) with source code, VST easily supports associating multiple specifications with a function and constructing multiple proofs for a given code/specification pair.
We write function specifications φ in the form {P } Y {v . Q} where v denotes the (sometimes existentially quantified) return value and P and Q are separation logic assertions. To shield details of its semantic model, VST exposes heap assertions using the type mpred rather than as direct Coq-level predicates. On top of mpred, assertions are essentially embedded shallowly, giving the user access to the logical and programmatic features of Coq when defining specifications. VST's top-level notion asserting that a (closed) program p -which must include main, with a standard specification -has been verified in Coq is p : G ("semax prog"). Here, G -of type funspecs, i.e. associating specifications φ to function identifiers f -constitutes a witnessing proof context that contains specifications for all functions in p and must itself be justified: for each (f, φ f ) ∈ G, the user must exhibit a Coq proof of G f : φ f ("semax body"), expressing that f satisfies φ f under hypotheses in G. VST's step-indexed model ensures logical consistency in case of (mutual) recursion.

VSU calculus
As described above, VST verification amounts to exhibiting a G with p : G. In contrast to VST's previous linking regime, VSU ensures existence of G during component linking without actually constructing G, maintaining representation hiding and non-exposure of private functions. Indeed, the modules' specification interfaces (specs of imported and exported functions) suffice for proving that a suitable G exists, as long as each module's individual justification includes the verification of its private functions.

Components and soundness
VSU extends CompCert's distinction between internal functions (those equipped locally with a function body) and external functions (functions defined in other compilation units, incl. system functions). Given a Clight compilation unit p, we denote these (disjoint) sets by IntFuns(p) and ExtFuns(p), respectively. VSU further distinguishes between system functions (typically provided by the OS) and ordinary external functions: the former ones are not expected to be verified using VST even in a fully linked program, so VSU merely records their use. VSU's main judgment is S , to be read as using specified imports I and system functions S, p provides/ exports functions (with specifications) E, using internal memory satisfying (initially) P . The entities S, I, and E are all funspecs, while P specifies the memory holding p's global variables; P 's formal type is globals → mpred where globals refers to a map from global identifiers to CompCert values.
The judgment S P [I] p [E] is formally introduced as an existential abstraction (in Coq: a Record type) over a proof context G, which is again of type funspecs: The role of G is to serve as the witness justifying the specification interface; as such it associates specifications also to p's private functions; existentially hiding it shields implementation details.  4. I ∪ G func funs p : G, 5. ∀ g, InitGPred(Vardefs(p)) g P g The first three clauses are largely administrative; they express, respectively, that (1) system functions and imported functions are disjoint sets of external functions, (2) G contains specifications for exactly the system functions and the internal functions, and (3) all exported specifications are abstractions of entries in G, in the sense of specification subsumption <:. Clause (4) constitutes the main proof obligation and refers to a slight refactoring of VST's function-verification judgment G 1 func funs : G 2 (semax-func), where funs associates CompCert function definitions with identifiers. The instantiation I ∪ G func funs p : G hence requires that imports I suffice for justifying all entries in G: each system function specification in G must be valid, and each specification of an internal function must be justified by a VST proof the corresponding function body in funs; calls to internal and system functions inside the body are resolved by reference to G, and calls to external functions are resolved by the import specifications, I.
Finally, clause (5) requires p's global variables to collectively satisfy P (after initialization) but avoids referring to these variables by name.
We point out two further aspects of Definition 1. First, we note that system functions may be exported (we do not require dom S ∩ dom E = ∅), and that imports and exports are distinct (dom I ∩ dom E = ∅ follows). Second, we note that for I = ∅, clause (4) yields G func funs p : G, i.e. the heart of VST's soundness condition semax prog for programs comprised of a single compilation unit. Hence, the goal of VSU verification is to exhaustively apply VSU's combination rule (presented in the next subsection) until all imports have been resolved.
Once a component has been verified and is exposed as S P [I] p [E], the specifications of p's private functions are hidden inside the existentially quantified context G and hence inaccessible.

Derived rules
It is easy to derive a rule of consequence from Definition 1 that strengthens imports and relaxes exports:

VSUConseq
For imported functions, we require pointwise subsumption, by defining I <: I to hold if dom I = dom I and I (i) <: I(i) for all i ∈ dom I. On the export side, we allow hiding of entries, by defining E E to hold if dom E ⊆ dom E and E(i) <: The calculus is invariant in the specifications of system functions, but allows weakening of the initialization predicate. The derivation of this rule instantiates the context witnessing the concluding judgment by the (abstract) witness obtained from unfolding the hypothetical judgment. VSU's workhorse is the composition rule, VSULink, shown in Figure 1. The side conditions treat the components symmetrically and are motivated as follows. The rule constructs a component specification for a linked program p that retains the internal functions of p 1 and p 2 , and also any unresolved external functions, as detailed in side conditions (b). Condition (c) requires functions classified identically by p 1 and p 2 to have identical definitions, and requires differently classified functions to have identical type signatures and be in the import set of the compilation unit not providing the implementation. Condition (d) formalizes that system functions are not locally defined in either unit. Condition (e) expresses that a function imported by one module and programmatically provided by the other module must be exported by the provider; this condition ensures that the export contract cannot be bypassed. Condition (f ) expresses that functions imported by both units must be imported identically -if necessary, this can be achieved using the consequence rule. Condition (g) calculates the remaining import specifications by combining the constituent imports, removing entries for the resolved functions, and ensuring the absence of duplicates. The final condition, (h), mandates that global variables from p 1 and p 2 be distinct (hence initialization predicates have disjoint footprints) and propagated to p. The most interesting aspect of the rule is the duplicate use of the intersection operator, C 1 C 2 , for constructing the concluding specifications of exported functions and system functions. The general definition of this operator is where ∧ denotes the specification intersection operator mentioned in Section 2. Thus, exporting E 1 E 2 effectively exports both E 1 and E 2 , and similarly for S 1 S 2 . Indeed, the individual export specifications can be reestablished using the consequence rule, as the properties of intersection specifications mentioned in Section 2 lift to (export specification) contexts: we have By permitting functions f that are internal to both p 1 and p 2 , VSU supports diamond-shaped composition patterns in which a sub-component, e.g. a library, is imported multiple times. Conditions (b) and (c) ensure that all copies of a repeatedly imported function f have the same body (i.e. CompCert AST), and that this body is retained in p. However, the library's export specification may have been imported differently by the different units, hence G 1 and G 2 may well associate different (and formally incompatible) specifications with f . As G 1 and G 2 are existentially hidden, we cannot inspect these specifications: adding a side condition to the rule that mentions the specifications G 1 (f ) and G 2 (f ) would violate the abstraction principle. Nevertheless, the proof of the composition rule still requires us to attach some specification to the shared function, when constructing the witnessing context of the concluding judgment, G. Our solution is to use intersection , i.e. to instantiate the witness G with G 1 G 2 in the Coq proof of VSULink. By terminating the Coq proof script with Qed rather than Defined, this instantiation is opaque to clients: applications of VSULink during program verification merely see that some G exists.
Most side conditions of the rule are computational; in our applications of the rule in Sections 4.5 and 5, Coq's tactical engine solves the majority of them.

APDs and specification interfaces
We now turn to the organization of predicates and function specifications. Our organization reflects typical realizations of abstraction principles in C, where heap data structures are introduced using forward declarations and referred to via pointers in header files, while the selection of a concrete representation (perhaps using private static variables) is private to an implementation. We illus-  trate our approach using Parkinson and Bierman's connection pool example [56], ported to C as an implementation of the APIs in Figure 2. Using forward declarations, the header files reveal only minimal information about the implementation. Connection.h allows clients to create a database entity (the parameter denotes a unique identifier; Parkinson and Bierman omit this constructor and do not model the type database explicitly) and to create connections to a database using the constructor consConn. Connectionpool.h models a collection of (dormant) connections associated with a database; clients construct a pool using consPool, request connections using getConn, and return them using freeConn. Figure 3 introduces abstract predicate declarations for the three data structures. Each APD declares zero or more spatial predicates, i.e. mpreds relating a Comp-Cert (pointer) value to suitable semantic information. Semantic information for the database is a DBindex (effectively a mathematical integer); connection and pool structures maintain pointers to the database; connections have additional internal state represented by the (abstract) type ConnTP. Specifically, DatabaseAPD corresponds to the Database type declaration in Connection.h and asserts existence of a predicate DB, together with an axiom that enables clients to store a reference to a database in their own data structure. Operator !! injects a Coq proposition into VST's assertion language.

Abstract predicate declarations (APDs)
In similar style, ConnectionAPD and PoolAPD declare predicates Conn and CPool for the struct declarations connection and pool. In contrast to Parkinson and Bierman, we model that the connection module maintains state using the predicate NextConn. There is no need to reveal the concrete static variable used by our implementation though: globals denotes the collection of all such variables in VST. We assert that the head values of Conn and CPool are provably nonnull pointers and that a Conn's head pointer is furthermore valid.
All APDs are introduced as (dependent) Record types in Coq. We will construct values of these types in Section 4.3, i.e. implementation-dependent concrete predicate definitions and lemmas validating the axioms. But first, we use the APD types abstractly to introduce specifications for the two modules.

Abstract specification interfaces (ASIs)
Abstract specification interfaces (ASIs) consist of VST specifications for the APIexposed functions, parametric in all relevant APDs. In addition to the APDs introduced above, our example uses a third APD, denoted M, that declares an abstract predicate Mem M gv and represents the malloc/free library. Figure 4 shows the ASI of Connection.h. We use subscripts to refer to the APD parameters: for example, DB D i p is the mpred obtained by applying the DB component of a database APD D to index i and pointer value p.
Post} is to be understood in safetyguaranteeing partial-correctness style, where x denotes a list of actual arguments (of type val), gv refers to (if present) the global environment, v (again of type val) represents the return value (if present), and other items are implicitly universally quantified. Callers of such a function select instantiations for the universally quantified entities ("witnesses") and must then establish Pre.
Thus, the specification of newDB asserts that a new database entity satisfying DB D i p is allocated at the return value p, for the database with index i (an input argument). The allocation draws upon the abstract predicate Mem M gv which is "located" at some global variable that is private to the malloc/free library.
The specification of constructor consConn refers to Mem M gv in similar fashion and advances the module's connection counter from c to some c upon success; in contrast to Parkinson and Bierman, we also support unsuccessful requests.
The ASI for Connectionpool.h in Figure 5 is additionally parametric in an PoolAPD, P. Our specifications are again slightly more precise than the ones given by Parkinson and Bierman. As a consequence, the precondition of a sequence such as p := consPool(s); c := getConn(s); freeConn(p, c) is DB D i d * Mem M gv * NextConn C s gv rather then emp, hence exposing the reliance on the memory manager etc..Prefixing the instruction d := newDB(i) establishes DB D i d; we will explain how the latter two conjuncts are provided in Section 4.5.

Verification of ASI-specified compilation units
Substantiating the ASI of a header file, means to give -for a concrete implementation -concrete definitions for the predicates in the newly introduced APDs, The ASI for Connectionpool.h is parametric in a database APD (D), a connection APD (C), a connection pool APD (P), and a memory manager APD (M). As consPool takes a formal parameter d, the reader may have expected the specification from the one given using VST's frame rule.
show that these definitions validate the associated axioms, and finally construct a VSU that has the ASI's specifications as the export interface E. All these constructions are parametric in the APDs provided by other modules. We refer the reader to our source code [14] for the C implementation, the (concrete) predicate definitions, and the proofs of the APD-supporting axioms. In case of Connection.c, these proofs reveal the instantiation of the APD's ConnTP to Coq's type of integers, Z, corresponding to the existence of a global integer variable in the C code that maintains a connection counter; the corresponding . . − → . . predicate then furnishes the abstract predicate NextConn.
The substantiations of a unit's APDs are subsequently used to instantiate its ASI and the specifications of its imported function, yielding (together with specifications of private functions) a proof context G that the unit's local function bodies are then verified against. APDs provided by other compilation units are left abstract, so expose only their axioms. Specifically, the substantiation for Connection.c yields values c and d of types ConnectionAPD and DatabaseAPD, respectively, the predicate N = NextConn c 0, and a VSU where E Conn is the partial specialization of the specifications in Figure 4 to C = c and D = d, Connection.prog is CompCert's AST for Connection.c, and I Conn contains a specification for surelymalloc. For ConnectionPool.c, we similarly obtain a value p of type ConnectionpoolAPD and a VSU where I Pool is comprised of the (abstract) specification of consConn and specifications for free and surelymalloc, and E Pool is the partial specialization of Figure 5 to P = p. Both VSUs are parametric in M, but VSU Pool 's additional parameters D and C are instantiated when VSU Conn and VSU Pool are combined using rule VSULink. The result, VSU CP , is still parametric in M but has resolved the imports of consConn, leaving only imports for free and surelymalloc.

A VSU for a malloc-free library
A recent application of VST is Appel and Naumann's verification of a malloc/free library [5]. Internally maintaining a fixed number of freelists -for entities of different size -this library exposes four functions in its API: malloc, free, prefill, tryprefill. When porting this development to the VSU framework, these give rise to two ASIs. The first one contains specifications for all four functions and is suitable for resource-aware clients. It employs the APD MallocFree -R -APD: memmgr -R models the freelists as a resource vector that indicates the length of each freelist. The predicate malloc token' refers to the piece of memory that is typically located at a small negative offset of a malloc'ed entity and holds administrative information of the library, but conceptually, it also constitutes a token that enables clients to share malloc'ed entities among different threads without loosing the ability to safely free entities. The second ASI only exposes malloc and free, and employs the more abstract APD MF -Tok still presents a malloc token but memmgr now hides the existence of freelists -indeed, constructing a MallocFreeAPD from a MallocFree -R -APD simply quantifies existentially over a resource vector. Our proofs first refactor the prior verification as a VSU that exports a resource-aware ASI and then use VSUConseq (and export restriction from Section 3.2) to weaken the resulting VSU to a VSU that only exports a resource-ignorant ASI. We denote the latter as VSU MF ; the predicate Mem M gv is now revealed to be a shorthand for memmgr gv, parametric in a MallocFreeAPD M, and we use Mem MF gv below to refer to its instantiation for VSU MF .

Putting it all together
Using VSULink again, we link VSU CP with a library VSU (reducing surelymalloc to malloc and the system function exit) and then with VSU MF , obtaining Here, coreprog contains all code (application plus library) with the exception of main. Note that VSU AppLib 's set of imports is empty; S Core contains axiomatic specifications of OS functions such as exit and mmap.
Independent from the construction of VSU AppLib we verify main, i.e. an exemplary client or unit test, as a semax body statement w.r.t. a not yet instantiated copy of E Core . The specification that main is verified against a <: specialization of VST's general mainspec but is still abstract in the APDs of the application's code modules -see [14] for details.
Finally, we connect VSU AppLib with the verification of main to obtain a proof of VST's semax prog statement. It is in this last proof that the satisfaction of the abstract initialization predicates for the global variables, Mem MF and N , is established from VST's internal initialization predicates. A typical example is the chain update → notify → get in the subject-observer pattern, a widely used design pattern [23] that has served as a litmus test for modular specification of callback-rich programming in the literature. Figures 6  and 7 contain excerpts of a transcription of Parkinson's [55] code into 1 C. Each Subject maintains a list of subscribers -a list of observers that will be notified whenever the Subject's state is updated and then synchronize their internal state accordingly using get. The intended invariants express that each Subject's observers are in sync -a property that is violated during update's traversal of its observer list, when not-yet-notified observers are out of sync but (precisely in order to get back in sync) nevertheless invoke get.

Modular verification of the Subject/Observer pattern
The dominant technique for dealing with such situations in SMT-based tools employs ghost fields that track validity and unfolding of invariants and are supported by further (ghost) infrastructure that controls ownership (see e.g. [47,11]). However, this does not necessarily achieve comprehensive representation hiding: for example, the permission to violate Subject's invariant in get's precondition propagates to the precondition of notify, allowing the latter function to access the field 2 Subject.value. Furthermore, the invariant-regulating techniques typically require that SMT solving be carried out on a whole-program basis.
The flexibility of APDs to introduce multiple predicates enables an alternative in which callbacks are specified using special-purpose predicates thatsimilar to typestates [62] -emphasize protocol-style behavior, do not reveal the validity of module invariants, and maintain representational hiding by being just as abstract as a module's main predicate. Concretely, our approach employs semantic subjects that are comprised of a list of observer references and a (current) value, while observers are represented as a subject pointer and the cache: Next, our APDs complement the predicates relevant for API calls by external clients, Srep and Orep, by (residual) predicates for calling the Subject functions registr, update, and get, and the Observer functions notify and val; we also intro-duce a predicate for the postcondition of get, GetPost: Axiom SubjGetPrePost splits Srep into a token that can (only) be used to invoke get, plus a token for reestablishing Srep from GetPost. The latter is a separating implication − * rather than an entailment: it represents the requirement that an observer yields back control to its subject after completing a callback to get -the subject had retained part of its state prior to invoking notify.
To enforce these behaviors, we employ the specifications in Figures 8 and 9; again, the ASIs are parametric in all APDs mentioned, notwithstanding the mutual dependence of the modules. Using axiom SubjGetPrePost, one may show that the specifications for get and notify are in subsumption relationship with large-footprint counterparts that permit invocations by external clients: The specification of update makes reference to an auxiliary Coq function that represents the "big" separating conjunction *   existentially abstracts over snd O but is otherwise identical to Orep. This makes validating axiom ObsNtfy trivial. As NtfyPre does not depend on a subject's value, no modification of the latter can affect the former's. Other residual predicateslike RegPre -are even definitionally equal to the main predicates, but the APD mechanism ensures that this fact is not exposed to clients. Our C implementation permits GetPre and GetPost to actually be defined identically (indeed, getters typically don't alter data structures. . Here, the p.π sh − → t v is a variant of p sh − → t v that specifies the content at p.π, where path π is a list of field names and array subscripts. Thus, GetPrePost only specifies the content of s.value; the remaining portion of s is exactly what is retained when SubjGetPrePost splits off GetPre from a Subject. The motivation for this handling is that the invariant of the loop in update (which contains the callback to get via notify) only traverses the node list. Specifically, an invariant involving the full Srep would not ensure that the spine of the list remains unchanged, as the definition of Srep quantifies existentially over the node list. This aspect illustrates the danger of predicates that are too abstract to be useful.
Constructing VSUs for Subject and Observer proceeds straight-forwardly; we exercise VSU's support for shared libraries by first combining surelyMalloc with each of these VSUs separately, before linking the resulting VSUs with each other, with VSU MF , and with a main client as described in Section 4.5.

Specification and proof reuse
To evaluate specification modularity and proof reuse, we verified several variations of our implementation. First, to evaluate robustness under representational change, we have Subject internally maintain a freelist of Observer nodes: struct subject { Node fl; Node obs; unsigned value; }; The freelist is drawn upon in registr (we only invoke surelymalloc if fl is null) and replenished in detachfirst. Constructor newSubject creates an empty freelist, and freeSubject frees the entire list.
The code modification triggers new Clight ASTs, but the majority of Coq files can then simply be reprocessed: the model-level definitions, APDs, and ASIs of Subject and Observer remain unchanged, and so do the files associated with verifying Observer, linking, and main. The calls to notify in update and registr obtain the additional argument &get, and the specification of get can be removed from the imports of the Observer VSU. The small specification of notify becomes where funcptr φ g expresses that value g is a pointer to some function satisfying specification φ, and φ get is the entry for get from Fig. 8. notify's large specification is adapted similarly. Repairing the proofs incurs changes in < 10 lines of Coq. A third modification exploits VST's support for impredicative quantification to abstract over GetPre SP and GetPre SP in the definition of φ, such that notify's specification is effectively parametric in suitable GetPre/GetPost pairs. Adapting the verification involves step-indexed aspects of VST and hence requires a little more work; details are included in the Coq development [14].
Finally, we verified a variation in which observers register with two subjects, as an example of a more complex interaction pattern. As this affects model-level functionality, modifications are not confined to module-internal predicate definitions but affect APDs declarations and ASI definitions. However, neither the encapsulation of representation nor the modularity of verification were compromised; supporting more than two subjects per observer would likely be similar.

Pattern-level specification
An alternative specification of subject-observer was proposed by Parkinson [55], who sidesteps the conflict between callbacks, modularity, and abstraction. Giving up on specifying the two classes independently, this approach defines a single abstract predicate, SubObs, that ties a subject to all its observers and yields aggregate-level function specifications. We can recover such an aggregate interface by proving that the specifications involving SubObs are abstractions (in the sense of . <: .) of the exports of the SubjectObserver VSU, generically in APDs SP and OP. Indeed, Parkinson's formulation amounts to a two-predicate APD: with specifications shown in Figure 10,  Constructing an AggAPD A from a SP/OP pair is trivial: take Sub to be Srep SP and Obs to be Orep OP ; proving the . <: . lemmas is then straight-forward. SubObs constitutes a pattern invariant, or the pattern's primary predicate, with residuals Sub and Obs. From the aggregate's point of view, update → notify → get is not a callback but an internal nesting of invocations, so the smallfootprint specifications typically don't pose a problem for existing methodologies; client-visible specifications with large footprints can be derived using the frame rule. In this sense, the pattern reestablishes "sequential atomicity" of operations. Exploring whether other design patterns can be similarly derived from the ASIs of their constituent classes is a topic for future research: are typical design patterns the abstraction units at which sequential atomicity is reestablished, callbacks at most occur in valid states, and residual predicates are avoided?
An aggregate specification for the function pointer implementation from Section 5.1 can be obtained using a modified AggAPD, with residual predicates GetPre etc.. But a better option is to remove the pattern-internal functions notify, registr, and perhaps even get from the aggregate ASI. In fact, notify's new signature reveals the use of function pointers, hence even an aggregate-level specification would have to include funcptr φ g terms. Thus, we instead employ the notion from Section 3.2 to lift the VSU for SubjectObserver with function pointers from Section 5.1 to a VSU for the aggregate but narrowed ASI and then reverify main w.r.t the latter.

Verification of object principles
This section considers features that -together with state encapsulation and modularity -are cornerstones of object orientation: the ability for (instances of) multiple implementations of an interface to dynamically coexist and interact, dynamic dispatch, subtyping, self, and inheritance. To maintain the dataless discipline, we employ a uniform but simple object encoding that is typical for industrial and open-source C developments: dynamic dispatch is implemented using function pointers that are bundled into separate structs (method tables) that are accessible as the first element of the object representations. Subtyping -providing additional methods -and representation inheritance are modeled by extending these structs, respectively, but are orthogonal to each other, and only the former one is exposed in APIs. In the second half of this section, we hide the dynamic dispatch mechanism behind a wrapper interface. We specify objects by reference to a semantic (Coq-level) object model, thus comprehensively separating object reasoning from C-level reasoning: constructors establish, and methods maintain, abstract object predicates that clients need not (and cannot) unfold.
We again proceed in stages, using the widely used running example of points located on a one-dimensional axis (see e.g. [18]). Figure 11 shows a preliminary API for basic, bumpable, and colored points, organized in a simple subtyping relationship. We provide multiple implementations for each interface (using dif-  Fig. 11. PointInterface.h, containing three interfaces for one-dimensional points ferent data representations), each exposing its set of constructors in a separate header file - Figure 12 shows implementation I1. Clients select an implementation during object creation but cannot otherwise distinguish between them: method dispatch selects the appropriate function from the method table, as in BPoint bp = makeBPoint -I1(4); int i = ((BMethods)(bp→mtable))→get((Point)bp)). The basis of object specifications is a general method table predicate: It asserts that the struct m (of shape k ) contains at field names names pointers to functions satisfying specs, where I is of Coq-type Pred(T) = (T * val) → mpred and specs has type list (Pred(T) → funspec). A generic object layout predicate then combines a specified method table (located at field tbl ) with the requirement that the (memory identified by the) object pointer satisfy I. C types σ and δ represent the object's static and dynamic types. The joint use of I in MTable and N ensures that an object's methods agree with its data component on what representation predicate should be maintained. The existential abstraction over I ensures representation hiding: external clients merely see a invariant of (Coq) type T . Thus, different C implementations of an object interface may employ different representations but still satisfy the same external specification. Specifically, we introduce Coq-level object interface types in the style of Hofmann and Pierce's object model [30]: The parameters X represent semantic object representations. On the one hand, we may instantiate these and define Coq-level behaviors, like m1, bm1, cm1: .
Here, point, bpoint, cpoint and methods, bmethods, cmethods are the structs defined in the header file ( Figure 11) and mtable, get, . . . , getC are the field names in these structs. The exemplary spec for base point constructors is then Verifying I1 and I2 then yields VSUs whose export interfaces tie makePoint -I1 makePoint -I2 to the specialization of this constructor to P := m1, and similarly for the other constructors. The resulting objects behave indistinguishably; the existential quantification over I in the definition of N carries over to P, B, and C, ensuring that the representational differences between I1 and I2 are hidden from clients: when verifying a method call, clients unroll P etc., but each time receive a "fresh" symbolic representation predicate I.
Wrapper-based verification The unrolling of object predicates corresponds to the exposure of the method table in our API. Programmatically, better encapsulation is provided by wrappers that hide the function pointer mechanism, like int GET (Point p) { Methods m = p→ mtable; return (m→ get(p)); } The header file for these wrappers resembles the API of an ADT, but merely disguises object-orientation: we still support multiple implementations (using the same constructors as above), and operations are still invoked using dynamic dispatch. On the specification side, wrappers can be modeled as an APD  with one constructor per interface, in resemblance to the use of class names to index predicate families [56]. The VSU for the wrapper then encapsulates the object predicates P etc., exporting an ASI with specifications such as GET(p) : We can further improve client-side usability by replacing these intersection specifications by a deep embedding of the three interface alternatives; this eliminates a corresponding case distinction in client-side proofs, when symbolic execution reaches the invocation of a wrapper function. As an example, we verified a linked list module that permits insertion of basic, bumpable, or colored points and provides map operations that apply SET, BUMP, . . . to all elements. Each element may internally employ I1 or I2. Of course, the precondition of mapping BUMP requires all elements to be of dynamic type (at least) BPoint and have a bumpable coordinate; however, this condition emerges as a constraint on semantic objects and can be discharged without unfolding object representation predicates.
Self and late binding Verification using the above constructions fails for methods whose body contains virtual calls on self : the definition of N effectively separates the object's data region from the method table upon method entry, making only the former accessible inside the body. To overcome this limitation, we define a variant of N using the higher-order recursive functor k , names, m, specs, X) in which I is now a parameter (we eschew the parameters T, . . . , specs for readability) and X plays the role of N . Recursion via X is protected by VST's [4] modality £; indeed, any access to a method table inside a method happens at least one step later than the method's own invocation. Contractiveness of F (proven in VST) ensures the existence of a fixed point F(I) := HORec(F(I)). Recovering the quantification over I, we then replace N with N * := ∃ I. F(I).
With this modification in place, one may verify virtual calls on self, like a variant of I1 that implements bump using get and set (still w.r.t. m1, bm1, and cm1).
An important application of self is (observably behavior-altering) method overriding. At the semantic level, Hofmann and Pierce explicate how positive subtyping supports both early and late binding variants of overriding; these differ in whether the observable behavior of bump (when implemented in terms of get and set) is affected when a subclass subsequently overrides set to, say, reset the coordinate to 0. Furthermore, method overriding may affect how functions defined in a superclass act on subclass-introduced state components. For example, one may impose that updating the coordinate turns a point's color blue. Semantically, all these variations yield novel behaviors m2, bm2, and cm2, etc. that can be compared to the earlier behaviors using Hofmann and Pierce's theory. As a consequence of our two-level reasoning, and the choice to parameterise constructor/method specifications by behaviors, we can leverage their techniques: implementations I3, I4. . . that realize the overriding variants can be verified as further VSUs for our earlier export interface, by (now) specializing the constructor specifications to m2, etc.. Afterwards, the modified behaviors propagate through dynamic dispatch and wrappers as expected, permitting clients of e.g. the list module to map bump over elements with different behavior. Side conditions during symbolic method calls refer exclusively to semantic objects and behaviors, do not necessitate the unrolling of representation predicates, and can often just be discharged using simplification.

Discussion
Related and future work Certified Abstraction Layers (CAL, [24,26]) are used in the CertiKOS project [25] to verify feature-rich operating system kernels and hypervisors in Coq. CAL permits horizontal and vertical composition of components, and establishes full abstraction between the imports and exports. CAL's methodology was recently rephrased as a synthesis from a systems-oriented DSL, DeepSEA, to C, with a CompCert backend [64]. However, "(T)here is no use of C pointers and no built-in support of dynamic memory allocation (every DeepSEA object is realized as a set of static variables), so programs that need dynamic allocation will have to implement it themselves" ( [64], page 10). While this fragment remarkably suffices for the intended application area, it is unlikely to satisfy general-purpose programmers or compiler writers for other systems languages.
Ironclad Apps and Ironfleet [29,28] are systems based on Dafny and TLA+ for verifying safety and liveness of distributed systems, and app security. By connecting model-level, concurrency-aware reasoning, state-machine refinement, and Floyd-Hoare verification, their approach provides abstraction-bridging functionality similar to that of proof-assistant-based reasoning, trading off TCB size and foundational integration in an logical framework against automation and developer productivity. Ironclad Apps compile to verified assembly; Ironfleet employs a formally unverified route via Dafny and the .NET compiler for C#.
Uberspark [67] is a system based on Frama-C and SMT for compositionally verifying commodity system software written in C and assembly.Überspark's primary applications are hypervisor components and OS kernels, but it currently addresses only safety and security properties (memory separation, control-flow integrity, information flow) rather than functional correctness. The same limita-tion applies to proof-carrying code systems [49,3,6,13,27], at (virtual) machine or assembly level. Several PCC systems proposed hierarchies of formalisms that connect operational semantics, a general-purpose program logic, and tactical checkers or algorithmic inference systems for higher-level type systems, abstract interpretation, or program analyses [16,2,17,1]. VST's tactical automation is optimized for symbolic execution and functional correctness, but the underlying proof rules could equally well be used to prove soundness of static analyses or code synthesizers; we expect our structuring principles for separate compilation will be just as useful in these scenarios as they are for functional correctness.
McKinna and Burstall [45] pioneered the use of existential abstraction to formally tie programs to their specifications and proofs in a modern proof assistant. VSU realizes aspects of their vision of deliverables for a mainstream language but is at this point not endowed with similarly rigorous categorical underpinnings.
Representation hiding in separation logic can also be obtained using hypothetical frame rules [54,10], but no such rule is provided by VST at present. Pragmatically, the two approaches appear complementary: modules that expose interesting state (e.g. a list ADT, the point objects,...) favor existential abstraction/APDs, as clients can access associated reasoning principles on demand, at specific program points. In contrast, modules like the resource-unaware memory manager might benefit from hypothetical framing: the predicate Mem M gv carries no client-relevant information but still needs to be carried around in many function specifications in our treatment.
VST's specification subsumption resembles behavioral subtyping [44,42], a notion commonly used in verification tools for Java-like languages for relating specifications across a class hierarchy. Exploring the relationship between our use of positive subtyping, other notions of subtyping and inheritance, and Liskov's Substitution Principle [43] constitutes future work.
By supporting field update, Hofmann and Pierce's theory addresses shortcomings of purely functional object models, but its support for object aggregates or complex ownership structures appears limited and not much studied. A twolevel encoding could likely also be developed for concurrency-inspired object models [33,32,31], perhaps by adapting the theory of interaction trees [68,39]. However, VST's partial-correctness interpretation of triples limits the end-to-end usefulness of coinductive reasoning. A recent proposal for integrating statically typed Smalltalk-inspired objects into a functional calculus is Wyvern [52].
In the context of SMT-based verification tools, Parkinson and Bierman [57] highlight examples that go beyond behavioral subtyping, and Summers et al. [66] identify a catalog of advanced uses of class invariants. We intend to apply VST to the former soon; a better understanding of the latter could perhaps commence by recasting Drossopoulou et al.'s general framework for object invariants [22] in separation logic. However, some aspects of class/object invariants may not immediately transfer from Java-like to Smalltalk-style object models.
In Java, an object's representation remains constant over its lifetime. By separately quantifying over I, our pre-and postconditions may support dynamic representation change a la Fickle [21] (with suitable updates to the method table), as long as both representations fit into an object's top-level struct.
Krishnaswami et al. [41] verify subject-observer and other patterns (iterators, flyweight, factory) by equipping a functional language, Idealized ML, with effectful specifications based on higher-order separation logic. Their verification was partially formalized in a predicative Hoare Type Theory/Ynot and employs abstract module definitions that combine code and specification. Their use of separating implication can likely be transferred to our setting, but their implementation does not separate the functionality of subjects and observers to same extent and thus does not raise the same specification challenges. Considerate reasoning [65], object propositions [51], and multi-object languages such as Rumer [9] are alternatives in the design space spanned by invariant techniques, aliasing/separation and ownership; all validate variants of Composite pattern.
Extrapolating from our exploration of the Composite pattern, it appears feasible to generate VST specifications, loop invariants, and APD declarations from Verifast [34]; synthesizing full proofs will be more challenging.
Object encodings in the Linux kernel, GTK/GObject, or the SQLite database engine deviate from the Smalltalk tradition and expose APIs that are not fully dataless. We suspect these systems also differ from standard language-level object disciplines in their need for deeply layered ownership control or model-level object aggregates. Like Schreiner's encoding [63], these systems thus provide interesting opportunities for future case studies.

Conclusion
The ability of type theory to capture modularity and abstraction is well-established. But while, e.g. Mitchell and Plotkin's insight has been highly influential in the world of functional programming, it has not yet made its way into verification tools for mainstream languages. Taking inspiration from their work, we introduced Verified Software Units as a general component calculus for VST, and developed an infrastructure for separating the declarations of abstract predicates from concrete predicate definitions. We showed that residual predicates support callbacks which violate operation atomicity, as is the case in the subject-observer pattern. Finally, we introduced a two-level approach to specifying object principles, yielding a simple logic for Smalltalk-style objects in C. Together, these innovations substantially advance VST's capability to verify modular C developments that employ diverse programming styles.