Keywords

These keywords were added by machine and not by the authors. This process is experimental and the keywords may be updated as the learning algorithm improves.

1 Introduction

In the past two decades, formal methods have been successfully applied in the verification of many critical systems. To improve confidence on the reliability of computer systems, verification of functional correctness and security properties is applied not only at the specification level [19], but also at the implementation [9] or even at the machine code level [5]. Verification of the implementation requires modelling languages that are able to capture the features in programming languages such as exceptions and procedure calls. Verification of sequential programs at implementation and machine code level has gained much attention both in academia and in industry [11], and now there is a reasonably strong tool support in this area [10, 15]. However, nowadays critical and high-assurance systems are often designed for multi-core architectures where multiple processes run in parallel, but verification techniques and tools for concurrent programs are relatively less developed than those for sequential programs.

In order to tackle the verification of concurrent programs, first Owicki and Gries’s work [14] introduced techniques for the verification of parallel programs. Later Jones [4] introduced the rely-guarantee method to improve Owicki and Gries’s method by allowing compositional verification. The Owicki-Gries method has been mechanized in the Isabelle/HOL theorem prover in [13], and Jones’s rely-guarantee method has been mechanized in Isabelle/HOL in [12] which follows the specification in [17]. Also, [1] models in Isabelle/HOL an algebraic specification of rely-guarantee. Although the languages used in previous mechanizations of the above mentioned methods are suitable for verifying system specifications, many implementations cannot be directly captured in those mechanizations. Therefore there is a need to develop a richer modelling language to accurately capture the behaviour of programs at the implementation level.

Simpl [15] is a while-language that supports most of the features of real world programming languages. The syntax and semantics of Simpl are modelled in Isabelle/HOL and Simpl has been used in the verification of seL4 source code [9]. However, its design aims only at reasoning about sequential programs, consequently, this language lacks constructors for parallel composition of programs. Moreover, its proof system is based on Hoare Logic, also for the verification of sequential languages, which cannot be used for reasoning about concurrent programs.

Building on the Simpl framework and the rely-guarantee method, we develop a formal verification framework in Isabelle/HOL, called CSimpl, for verifying partial correctness of high-assurance concurrent systems. The main contributions of this paper are as below:

(1) We extend Simpl using the notion of computation [12, 17] to introduce parallelism in two layers: the bottom layer is the execution of sequential Simpl programs extended with a synchronization primitive Await over shared variables; and the top layer is the parallel execution of the bottom layer programs by means of a parallel composition operator. While existing rely-guarantee methods are mechanized for reasoning about abstract specification languages [12, 13], our method goes one step further and covers most of the features of system programming languages such as exceptions, procedures, and pointers, among others.

(2) We define a compositional semantics of rely-guarantee for CSimpl. We also provide a set of inference rules for the rely-guarantee proof system and we prove that they are sound w.r.t. the semantics. The rich expressibility of CSimpl means that the number of inference rules of the rely-guarantee proof system is much higher than the work in [12] and their complexity is significantly increased. The CSimpl semantics, the rely-guarantee proof system specification and its soundness proof comprise more than 15k lines of proof and specification in Isabelle/HOL and IsarFootnote 1.

(3) As a case study, we specify in CSimpl two XtratuM [3] services for queuing inter-partition communication and we prove the correctness of an invariant on the queuing communication structure. Inter-partition communication is the mechanism used to implement information flow and is critical in proving event-based non-interference. XtratuM is a separation micro-kernel for space and time partitioning of applications. XtratuM supports multi-core architectures, being able to run several instances of the micro-kernel in parallel in multiple cores. Using our new rely-guarantee proof system, we prove that the specification of the inter-partition communication services correctly introduces and removes messages in the communication channel. The specification and the proofs comprise 3500 lines of formalization. To the best of our knowledge, this is the first attempt on the verification of separation micro-kernels targeting multi-core architectures. Other works such as [18,19,20] verify functional correctness and non-interference for sequential micro-kernels, and the work in [2] focuses on the verification of sequential applications using the ARINC standard.

2 CSimpl Language

2.1 Simpl Overview

Schirmer introduces in [15] a verification framework for imperative sequential programs developed in Isabelle/HOL. The verification framework includes a generic imperative language, called Simpl, which is composed of the necessary constructors to capture most of the features present in common sequential languages, such as conditional branching, loops, abrupt termination and exceptions, assertions, mutually recursive functions, expressions with side effects, and nondeterminism. Additionally, Simpl can express memory related features like the memory heap, pointers, and pointers to functions. The Simpl verification framework also includes a Floyd/Hoare-like logic to reason about partial and total correctness, and on top of it, the framework implements a verification condition generator (VCG) to ease the verification process.

In order to capture all aspects of abrupt termination, assertions, and function calls, the program state ’s in Simpl is modelled in Isabelle/HOL as a datatype xstate (shown in Fig. 1), which is composed of four different constructors: Normal ’s, representing a regular execution; Fault ’f, representing a failed assertion; Abrupt ’s, representing an exceptional state; and Stuck, representing a state where a call to a non-defined function is made. Additionally, the semantics requires an environment \(\varGamma \) containing procedure definitions, i.e., a partial function from the set ’p of procedure names to the body of the procedures. Both features regarding the state and procedures definitions are used in CSimpl.

Fig. 1.
figure 1

Syntax and state definition of the CSimpl language

2.2 CSimpl Syntax

The syntax of CSimpl is shown in Fig. 1. CSimpl extends Simpl by adding two constructors for concurrency: Await, which takes two parameters cond and body, and Parallel Composition. Await allows synchronization of processes under the boolean condition cond and then it atomically executes body, which is a pure sequential Simpl program. This allows us to use Hoare logic for sequential programs and the original Simpl VCG in the verification of the atomic blocks. Parallel composition happens at the top layer (root program), and it can not be nested with other constructors like in the approach followed in [7]. Therefore, a parallel program launches n sequential programs that are executed concurrently and that do not create new concurrent threads. A parallel program is defined as a list of sequential programs. Since we are aiming the verification of programs without dynamic creation of process, this approach is not a problem for our goal and simplify the mechanization.

CSimpl’s syntax, following the syntax of Simpl, is defined in terms of states, of type ’s; a set of fault types, of type ’f; and a set of procedure names of type ’p. The constructor Skip indicates program termination; Seq s1 s2, Cond b c1 c2, and While b c are respectively the standard constructors for sequential, conditional, and loop statements. Throw and Throw c1 c2 are the complements for abrupt termination of programs of Skip and Seq c1 c2, and they allow to model exceptions. Call p invokes procedure p; Guard f g c represents assertions, where c is executed if the guard g holds in the current state, fault of type ’f is raised otherwise. Finally, Spec r and DynCom cs respectively introduce a nondeterministic behavior expressed by relation r, and a state dependent dynamic command transformation which is used to model blocks and functions with arguments.

2.3 CSimpl Semantics

The small-step operational semantics of CSimpl is a predicate inductively defined based on an environment for procedures \(\varGamma \) and a pair of component configurations ((P,s), (P’,s’)) where the program P in the state s, transits to the program P’ and the state s’. It is represented as , where c indicates it is a step transition in CSimpl. A CSimpl component configuration is defined as a tuple (P,s) where P is a CSimpl program and s is of type xstate. A component configuration (p,s) is called final if \(\mathtt{p}=\mathtt{Skip}\) or \(\mathtt{p}=\mathtt{Throw}\) and there exists a state \(s'\) such that \(s= \mathtt{Normal\ s'}\). A final configuration cannot progress to another configuration.

CSimpl extends Simpl with rules for synchronization on shared variables, Await, and the parallel computation shown below. For space reason we only provide the small-step semantics rules Await and AwaitAb for the Await command (Fig. 2). The rest are similar to those defined in [15].

Fig. 2.
figure 2

Small step and environment CSimpl semantic rules

The Await rules leverage Simpl’s big step semantics to atomically transit from the initial configuration (p,s) to the next state t resulting from the execution of p from s and it is expressed as . The two rules express the situation where from a current state s satisfying the synchronization condition. The atomic program in Simpl ends in a state t that can be an abrupt state as a result of an exception thrown in the sequential program for the rule AwaitAb, or any other possible state for the rule Await. This distinction is necessary since a Simpl program can finish in an Abrupt state, however the small-step semantics does not use the state Abrupt. Instead, a CSimpl program finishes in an exception state when the last configuration of a computation is a pair composed of the program Throw, together with a Normal state. Note that big step transitions use sequential Simpl programs, therefore the environment in the atomic step has to be a function from procedure names to Simpl programs, which do not contain Await instructions (for the same reason the body of Await cannot contain nested Await neither). translates bodies of procedures in into Simpl programs if they do not contain any Await instruction, removing from those procedures containing Await instructions.

A Parallel CSimpl configuration is defined as a tuple (Ps,s) where Ps is a list of CSimpl programs and s is of type xstate. Parallel CSimpl semantics is inductively defined by means of rule PAR in Fig. 2. A parallel configuration (Ps,s) transits to another parallel configuration (Ps[i:=r], s’) when there is a program i in Ps such that . It is represented with . Similarly to component configurations, a parallel CSimpl configuration \(\mathtt{(xs,s)}\) is called final if xs is not empty and every component configuration \((\mathtt{xs}!i,s)\), with i smaller than the length of xs, is final. Ps!i means accessing the i element in the list Ps, whilst \(Ps[i:=r]\) means substitute the i element in Ps for r.

Together with the semantic representing component transitions, it is necessary to define semantics for environment transitions. They are inductively defined using rules Env and Env_n in Fig. 2, where e is to express that it is an environment transition. CSimpl semantics for components can transit from a Normal state to a different type. However it is not possible to transit from a non Normal state to a different type of state, i.e. \(\varGamma \vdash _{p}\mathtt{(P,Stuck)} \rightarrow \mathtt{(P',Normal\ t)}\). Moreover, the component semantics always transits from a configuration (p,s) with \(\mathtt{p} ~= \mathtt{Skip}\) and \(\not \exists \mathtt{s'. s =Normal\ s'}\) to a final transition (Skip,s). Therefore, the environment at the sequential layer needs to model this behaviour in the rules Env and Env_n in order to make the semantics at the parallel layer compositional. Environment transitions at the parallel level are defined in such a way that they can transit from a Normal state to another Normal state as shown in rule P_ENV in Fig. 2.

3 Rely-Guarantee for CSimpl

The rely-guarantee [7] method extends the specification of a program with two relations R and G characterizing, respectively, how the environment interferes with the program (Rely) and how the program modifies the environment (Guarantee). Therefore a specification for the verification of parallel systems using rely-guarantee is composed of four elements: precondition, postcondition, rely, and guarantee.

In order to take into account CSimpl state specification xstate (which can take multiple forms to express different execution issues), the semantic for procedure calls, and the dual postcondition for normal or exception termination, the rely-guarantee specification and proof rules need to be modified accordingly. In the proof system itself, a total of 8 new rules have been added to the work in [12] to deal with all the language constructors present in CSimpl. Finally, soundness of the axiomatic rules for the proof system w.r.t. the rely-guarantee specification of validity is proven. The multiple forms of states makes the proof considerably more complex and larger than the work in [12]. While the work in [12] consists of around 2300 lines of proofs and specification, the current work consists of more than 13000 lines of proofs and specification.

3.1 Definition of Computation for CSimpl

The formal validity of a rely-guarantee tuple in this work is based on the definition of computation, which is the set of all possible sequences of configurations resulting of transiting the component or the environment, starting from an initial configuration.

Definition 1

(Sequential Component Computation). A computation is a tuple \(\mathtt{(\varGamma , confs)}\) where \(\mathtt{\varGamma }\) is an environment for procedures and confs is a list of sequential configurations. The set of possible computations cptn is inductively defined as follows:

  • if

  • if

Definition 2

(cp \(\varGamma \) P s). The set of possible computations of an environment for procedures \(\varGamma \) starting from an initial configuration (Ps) is the set of tuples \((\varGamma ,l)\) such that \(l!0=(P,s)\) and \((\varGamma ,l)\in cptn\).

The set of parallel computations par_cp is defined similarly to cp using parallel configurations and the semantic rules for parallel and environment step transitions.

Definition 3

( ). Conjoin [17] represented by , defines an equivalence relation between a parallel computation p of n CSimpl components and a list clist of n component computations, where for all \(i<n\). \(clist!i=(\varGamma _i,cptn_i)\). (\(\varGamma \), p) clist iff:

  • for all \(i<n\), \(length\ cptn_i = lenght\ p\) and \(\varGamma _i= \varGamma \).

  • for all \(i<n\) and \(k<length\) p, \(cptn_i!k = (c^k_i, s^k_i)\) and \(p!k=(cs,s)\) with \(cs!i = c^k_i\) and \(s=s^k_i\).

  • for all k such that \(k+1<length\) p, if \(\varGamma \vdash _p p!k \rightarrow _e p!(k+1)\), then for all \(i<n\), \(\varGamma _i\vdash cptn_i!k \rightarrow _e cptn_i!(k+1)\); if \(\varGamma \vdash _p p!k \rightarrow p!(k+1)\) then there exists an \(i<n\) where \(\varGamma _i\vdash cptn_i!k \rightarrow (cptn_i)!(k+1)\) and \(\forall j.\ j\ne i \longrightarrow \) \(\varGamma _j\vdash cptn_j!k \rightarrow _e cptn_j!(k+1)\).

The last condition of conjoin states that for any step k in p, if k is an environment step in p, then k is also an environment step in all \(cptn_i\); and if it is a component step, then there is some \(cptn_i\) where k is a component step and for any other \(cptn_j\), with \(j\ne i\), k is an environment step.

Lemma 1

(Parallel computation as component computation).

$$\begin{aligned} \begin{aligned} xs\ne [] \Longrightarrow par_cp\ \varGamma xs\ s =&\{(\varGamma _1,c). \varGamma _1 = \varGamma \wedge (\exists clist.(length\ clist)=(length\ xs)\ \wedge \\&(\forall i<lenght\ clist. (clist!i)\in cp \varGamma (xs!i) s)\wedge (\varGamma ,c) {\propto }clist)\} \end{aligned} \end{aligned}$$

Lemma 1 states that given a parallel configuration \(\mathtt{(xs,s)}\) such that xs is not empty ([]), then for any parallel computation \((\varGamma ,\mathtt{c})\) starting from \(\mathtt{(xs,s)}\) there is a list of component computations \(\mathtt{clist}\) with the same length of \(\mathtt{xs}\) and \((\varGamma ,\mathtt{c})\, {\propto }\, clist\). That is, the execution of a parallel number of components \(xs_0 \dots xs_n\) can be expressed as the execution of one single component \(xs_i\), with i smaller than n, where the execution of any other component \(xs_j\) is simulated by a component environment transition, with j smaller than n and different than i. The right and left implications of the equality in Lemma 1 are proven first by induction on the parallel computation and then by cases on the type of parallel and component events using conjoin.

3.2 Validity of Formulas for Rely-Guarantee in CSimpl

Based on the rely-guarantee definitions, we define the validity of a rely-guarantee tuple from the set of all possible computations from an initial configuration. This uses the notions of assumption of preconditions and the environment, and commitment of the component and the postcondition.

Definition 4

(assum(pre, rely)). The assumption of a predicate pre and an environment relation rely for an environment of procedures \(\varGamma \) is the set of component computations \((\varGamma , cptn)\) such that \(cptn!0 = Normal\ s\) and \(s\in pre\), and for any step transition in the computation \(\varGamma \vdash _c cptn!k \rightarrow _e cptn!(k+1)\), where \(k+1<lenght\ cptn\), \(cptn!k=(p_k,s_k)\), and \(cptn!(k+1)=(p_{k+1},s_{k+1})\) then \((s_k,s_{k+1})\in rely\).

The predicate assum represents the set of component computations \((\varGamma , \mathtt{cptn)}\), such that the state component of the initial configuration of the computation is a Normal state satisfying pre. Also, in any transition of the environment \(\varGamma \vdash _c \mathtt{cptn}!k \rightarrow _e \mathtt{cptn}!(k+1)\), the states of the configurations \(\mathtt{cptn}!k\) and \(\mathtt{cptn}!(k+1)\) belong to the rely relation.

To take advantage of automatic methods such as model checking, and following the original notion of validity for Hoare triples in Simpl, the commitment assumes that the last configuration in a computation does not end in a Fault state belonging to the set F, which is a set of non-reachable states previously calculated using external tools. Then the commitment is the set of computations such that component transitions belong to the guarantee relation, and that their last configuration are final (therefore with the program state equal to Skip or Throw) with the state component belonging to q or a.

Definition 5

(comm(guar, (q, a)) F). The commitment of a relation guar, a pair of predicates (qa), and a set of Fault states F, for an environment of procedures \(\varGamma \), is the set of component computations \((\varGamma , cptn)\) such that if \(cptn !(length\ l - 1)=(l_p,l_s)\) and there is not a fault f such that \(l_s = \mathtt{Fault}\ f\) and \(f\in F\), then (1) if for any component transition in the computation \(\varGamma \vdash _c cptn!k \rightarrow cptn!(k+1)\) where \(k<length\ cptn\), \(cptn!k=(p_k,s_k)\), and \(cptn!(k+1)=(p_{k+1},s_{k+1})\) then \((s_k,s_{k+1})\in guar\), and (2) if l is final then \(l_s = \mathtt{Normal}\ l_s'\) and if \(l_p = \mathtt{Skip}\) then \(l_s' \in q\) and if \(l_p = \mathtt{Throw}\) then \(l_s' \in a\).

Definition 6

(com_validity). Validity of a specification of a component P w.r.t. a precondition p, postcondition (q,a), a rely relation R, a guarantee relation G, an environment of procedures \(\varGamma \), and a set F of Faults, is represented as \(\varGamma \models _{/F} P\ sat[p,R,G,q,a]\) iff for all s, \(cp\ \varGamma \ P\ s \cap assum(p,R) \subseteq comm(G(q,a))\ F\).

Following [15], we use a set of procedure specifications \(\varTheta \) that are used during the procedure verification. The set of procedure specifications \(\varTheta \), is a tuple which elements represent a procedure name and its specification in terms of precondition, rely and guarantee relations, and postcondition. Note that procedures in specifications belonging to \(\varTheta \) do not need to match the procedures defined in the environment \(\varGamma \).

Definition 7

(com_cvalidity). CValidity of a specification of a component P w.r.t. a precondition p, postcondition (q,a), a rely relation R, a guarantee relation G, an environment of procedures \(\varGamma \), a specification of procedures \(\varTheta \), and a set F of Faults, represented as \(\varGamma ,\varTheta \models _{/F} P\ sat[p,R,G,q,a]\) iff for all tuples \((c,p',R',G',q',a')\in \varTheta \) such that \(\varGamma \models _{/F} (Call\ c)\ sat[p',R',G',q',a']\) then \(\varGamma \models _{/F} P\ sat[p,R,G,q,a]\).

Validity and CValidity for parallel computations are respectively represented by \(\varGamma \models _{/F} P\ SAT[p,R,G,q,a]\) and \(\varGamma ,\varTheta \models _{/F} P\ SAT[p,R,G,q,a]\). They are defined similarly to the ones for computation of components, using the definitions of computation, assumption, and commitment for parallel programs. We omit these definitions due to space reasons. Theorem 1 shows compositionality of validity of parallel rely-guarantee specifications.

Theorem 1

(validity_compositionality).

figure a

Therefore, to show that a parallel rely-guarantee specification is true, it is only necessary to prove rely-guarantee validity for each one of the single components and that (1) the rely of individual components is implied by the parallel rely and the union of the guarantee relations of the other individual components of the parallel system, (2) the union of the guarantee relations of the component specifications implies the guarantee relation for the parallel specification, (3) the precondition of the parallel specification is included in all the component specifications, (4) the intersection of all the normal postconditions of the component specifications is included in the normal postcondition of the parallel specification, (5) the union of the abrupt postcondition of all the component specifications is included in the abrupt postcondition of the parallel specification. Theorem 1 is proven using Lemma 1 and the definition of parallel validity of a rely-guarantee specification.

Fig. 3.
figure 3

Rely-guarantee proof rules for CSimpl

3.3 Inference Rules of the Proof System

The rely-guarantee proof system for CSimpl extends the previous mechanization of the logic in [12] with eight more inference rules. There are a total of fifteen rules, one for each language constructor, plus the consequence rule. Figure 3 shows those rules that are either new or substantially changed w.r.t. the work in [12]. Rules Skip, and Throw are added to handle program termination for normal and abrupt termination respectively. Since Skip deals with normal termination it requires the normal postcondition to be stable w.r.t. the rely relation, whilst in the case of Throw is the abrupt postcondition which has to be stable w.r.t. the rely relation. Similarly, Catch is the complement of the sequential rule for abrupt termination. In CSimpl, composition of programs can finish on an abrupt state without executing the second program. Hence it is necessary stability of the abrupt postcondition w.r.t. the rely relation. Similarly, the Catch rule requires stability of the normal postcondition with rely. We say that a predicate p is stable w.r.t. a relation R, Sta p R, if given two states s, s’, such that p s is true and \(\mathtt{(s,s')}\in \) R, then s’ is also true in p.

The Await rule requires Hoare satisfiability of the sequential program representing its body, which is represented following traditional Hoare triplet notation \( \{p\} c \{q\}\). In this case the postcondition is given as a pair to capture normal and abrupt termination. See [15] for more details on sequential Simpl program verification. Since the Hoare program can finish in either a normal state or an abrupt state, it is necessary that both postconditions are stable. The precondition should also be stable to remain unchanged under environment transitions. Since every component transition has to belong to the guarantee relation, we add this constraint into the Hoare triple, binding the initial states from the precondition to the final states of both the normal and abrupt postconditions.

The rest of rules can be deduced intuitively from their semantics adding stability of the precondition for non-terminal commands, e.g., if for branching and call for non-recursive procedure calls; and adding also stability of the normal postcondition for commands modifying the state.

Finally COMP is the rule for parallel composition. To apply compositionality, the rule is applied over a tuple xs composed of a sequential component Com and rely-guarantee specification, i.e. PreRelyGuaranteePost for Com. The rule follows [12] taking abrupt termination into consideration, since this is an exception state, and not all the individual computations may be in an exception state. Therefore, whilst we require that the intersection of all component postconditions is included in the postcondition of the parallel program, for abrupt termination we only require that the union of abrupt postconditions is in the parallel program.

3.4 Soundness of the Proof System

We prove that the set of inference rules in the proof system is sound w.r.t. the definition of validity for parallel systems. The proof is carried out in two steps, first we prove that the inference rules for single components are sound.

Theorem 2

(comp_rgsound).

\(\varGamma , \varTheta \vdash _{/F} c\ sat\ [p,R,G,q,a] \longrightarrow \varGamma , \varTheta \models _{/F} c\ sat\ [p,R,G,q,a]\)

This is proved by induction on the inference rules. Axioms, i.e., those rules without assumptions on the proof system induction, are proven based on the notion of stability and the fact that any computation starting from them only has one component step. Therefore we prove that the stability rule preserves the precondition under any environment step. We then show that the component step preserves the commitment.

The semantics for computation makes it cumbersome to prove the soundness for those CSimpl constructors whose semantic is recursively defined, such as Seq, Catch, and While. Soundness for these constructors are proven using a modular notion of computation [17] and the equivalence of both types of computation. The modular computation serializes the recursive specification of computation for the CSimpl constructors. This alternative semantics for computation unfolds the computation of CSimpl constructors. Soundness for these constructors is proven based on the different cases these rules provide. The modular computation for CSimpl extends the one provided in [12] with rules for the new language constructors, and new rules for seq and while, considering that the program in a final configurations can be Skip or Throw. The constructors If and Call, for non-recursive function calls, are proven similarly to the axioms based on the existence of a first component step for non-final configurations. After applying the component transition, we prove the correctness by the inductive step.

Recursive procedure calls require to consider the maximum number of nested function calls invoked by an execution and we do not currently provide a rule for them. cptn serializes the small step semantics removing scopes, which does not allow to prove soundness of recursive procedure calls. Nevertheless, it is possible to provide such a rule for recursive procedure calls, by extending the modular computation, with a parameter n representing the limit of nested procedures for which the computation is valid. Also, the semantics for validity must be extended to express that a formula is valid when it invokes at least n nested function calls by intersecting the assumptions in com_validity with the set of modular computations with limit n. Soundness of recursive procedure calls can be proven similarly to [15], by monotonicity of the extended computation in n and equality of the semantics.

Finally we show soundness of the proof system for the parallel composition of programs using Theorems 1 and 2.

Theorem 3

(par_rgsound).

\(\varGamma , \varTheta \vdash _{/F} Ps\ SAT\ [p,R,G,q,a] \longrightarrow \varGamma , \varTheta \models _{/F} (ParCom\ Ps)\ SAT\ [p,R,G,q,a]\)

4 Case Study

We apply the proof system for the specification and the verification of two XtratuM services for inter-partition communication. The XtratuM separation micro-kernel [3] provides spatial and temporal partitioning of applications. In a separation micro-kernel, different partitions are executed in separated memory domains, and the only allowed communication among partitions is by means of static dedicated channels explicitly defined between two or more partitions. XtratuM provides, among others services to communicate partitions through channels, partitions health-monitoring, and a static cyclic scheduler. In this case study we provide a very abstract CSimpl specification of the services to send and receive messages using queuing channels in a parallel architecture, where the XtratuM micro-kernel is executed in several cores of a multi-core processor. Using the rely-guarantee proof system introduced in Sect. 3 we prove: (1) that the services correctly introduce and remove elements in the queues associated with each communication channel and (2) that the number of elements in the queues do not exceed the channel maximum capacity. The specification and proofs are comprised of more than 3500 lines of specification.

4.1 Queuing Inter-Partition Communication Description

Queuing inter-partition communication services allow partitions to escape from the isolated environment that XtratuM provides, allowing them to send and receive messages to/from other partitions using communication channels by means of dedicated ports assigned to partitions. A communication channel is an entity storing the communication data and the source and destination ports involved in the communication.

XtratuM implements two types of communication: sampling and queuing communication. While the former only allows to store one message, and it is multicasting, i.e., a channel has one source port and a list of destination ports. The latter allows to store many messages in a bounded buffer implemented as a queue, and only allows peer to peer communication, i.e., the channel has one source port and one destination port. Channels and ports are classified according to the type of communication. Therefore, a channel and a port can be of type sampling or queuing, and a sampling channel can only allocate sampling ports, and vice-verse. The services have as input a port to/from which the message is sent/received, and the message to be sent in the case of the sending service. Prior to modifying the queue, the services check whether the input values are consistent, e.g., the port which receives the message belongs to the partition, or it is a source or destination port depending on the invoked service.

4.2 State and Specification Definition

The state definition provides global and local variables. Global variables represent those variables shared by multiple instances of the micro-kernel, they hold the data for inter-partition communication, partitions, and the partition scheduler. Since we are targeting only queuing inter-partition communication, the components for the scheduler and partitions contain the necessary information for those services. The scheduler is highly abstracted and only contains information about the partition that is currently being executed, and therefore invoking the service; partitions only contain the list of assigned ports to the partition. The communication datatype includes the specification of channels and ports. A channel is defined as a datatype with the two possible types of channels, having as parameters the source and destination ports, and the message shared between the partitions, for which the queue is abstracted in the model as a multiset. The queuing channel also has a parameter indicating the maximum size of the channel buffer. Messages are modelled just as an abstract entity.

figure b

In the model, , , , and access the locals, communication, scheduler, and partition component of the state, respectively. Local variables for each process are a structure with the necessary variables for the input and output parameters of the services. One of the limitations of rely-guarantee is that the relations lose track of the sequence of executed operations. To solve this, verification of the concurrent increment of a variable, or adding/removing elements from a set like in this example, requires using additional variables to help tracking the changes [17]. In our model, we include a variable of type Message option that is initialized to None, and when a message is correctly sent or received the model assigns it to the variable. Our state abstracts and maps XtratuM global structures xmcCommChannelTab, and xmcCommChannelPorts, storing channel and port data, into the components of and xmcPartition, storing partition data, into  respectively.

The parallel execution of services is modelled parametrically on the number of processes, which is defined as a fixed natural number within a Isabelle/HOL locale [8]. Each service is modelled as a procedure that is also parametrized by the process being executed; this allows that each specific procedure only accesses its local variables. The function \(\varGamma \) is generated by assigning a unique name for each service using a fold higher order function and assigning to each parametrized service name the corresponding parametrized body of the service. The parametrized event receive is shown below.

figure c

The services abstract the low level behaviour of the Xtratum functions. They first check parameters validity, and then carries out the insertion/extraction of the message to/from the queue. Atomic blocks abstract XtratuM’s mutexes for mutual exclusion. Validation of correctness of the model w.r.t. the implementation is carried out at this stage by inspecting the code. For the ReceiveQueuingPort model, the event first checks that the accessed port exists in the current communication state, that it is a queuing and destination port, and that the partition that it belongs to the partition being executed. If any parameter is not valid then the service finishes returning None, otherwise it performs the operations over the channel queue after checking whether the queue is not empty for event receive, or not full for event send. The statements in the body of the Await statement are and . This is because the body of the Await is a sequential Simpl program and when embedded into a CSimpl program needs to modify the syntax. Event SendQueuingPort is modeled similarly.

4.3 Verification

For the parallel verification, we specify the rely and guarantee relations for the receive and send services. This relations are parameterized by a variable i, which refers to the ith process. We show the rely relation, the guarantee relation is similar to this, only differing in that local variables for any process j different than i will not be modified.

figure d

Since parallel programs do not change others programs’ local variables, the i element in the list of local variables is not changed by the rely. Also, ports, partitions, and the scheduler are not changed by any service, therefore the rely relation does not change them. Finally, if the initial state of the relation preserves the invariant, it also preserves the shape of the channel’s queues, then so does the final state of the relation.

The invariant establishes consistency of the port and channel structures that must be preserved by the services. Its most important specification is channel_spec which preserves the specification of the queue for every defined channel in the state.

figure e

channel_spec checks that the multiset modelling the queue for each defined c_id is equal to its initial value, which is given by B c_id; those messages correctly sent are pushed into the queue by the service; and that the received messages are popped out of the queue. chan_sent \(\backslash \) rec_msgs gets for each c_id the multiset with the auxiliary variables different than None, meaning that the service has modified the queue for that channel. Also, for consistency of the multiset, the invariant needs to ensure that removed messages are a subset of the added messages.

Lemma 2

(Send_Rec_Correct).

figure f

Lemma 2 proves the property on the parallel execution of the services. ex_service is a sequence of nested ifs controlling the call to the services, each if guarded by a local variable that indicates which service is invoked in each parallel process. In the parallel program, the identity relation indicates that the parallel environment does not change the state, being therefore a closed system, i.e., there is not any environment at the parallel level. The guarantee relation is the universal set in which everything can be modified. The precondition Pre_Arinc B defines the invariant and auxiliary variables initialization to None. The precondition for each process pre_i B i sets the initial value for the auxiliary variable, the initial values of the channel queues, and it defines the invariant that is preserved by the postcondition for the normal termination Post_Arinc B. The abrupt postcondition is the universal set since we do not have any abrupt termination in this specification.

The proof obligations for the parallel rule are proven immediately after unfolding the definitions of the precondition, postcondition, and rely and guarantee relations. After applying the parallel rule on the parallel execution of the n components, it is necessary to prove that the parametrized execute_service satisfies the postcondition using the rely-guarantee rules for components. Once the conditional and call rules have been applied on execute_service, only the proof of the verification of each service body is left. Both send and receive services are similarly proven.

To prove the body of the services, it is necessary to apply the conditional rule to generate the proof obligations for the execution of two branches of the if. The first corresponds to the case in which the service is not invoked with the appropriate parameters and is immediately proven after apply the Basic rule since it does not modify any channel or auxiliary variable. For the second branch, after invoking Await, the sequential Simpl program representing its body is automatically unfolded using Simpl’s VCG. The resulting goal, now without any embedded Simpl specification, is solved by proving that the state after removing or inserting a message from/to the channel associated to the input port, and after assigning the removed/inserted message to the auxiliary variable, satisfies channel_spec. We use some auxiliary lemmas to prove it: first, that the modification of the auxiliary variable in a component does not modify the sets chan_sent_msgs and chan_rec_msgs for any channel other than the one associated to the port the service access; second, that the modification of a variable only modifies one of these sets. Using these auxiliary lemmas the postcondition is proven immediately by applying the properties over multisets.

5 Conclusions and Future Work

In this work we have presented CSimpl, a framework for specifying concurrent programs and verifying their partial correctness using rely-guarantee. This framework allows us to specify programs written in a large subset of the C language. Currently we are working also on axiomatic separation rules for the proof system following works on separation logic and rely-guarantee [6, 16]. This will help to cope with local variables and to hide global variables, thus improving scalability of the approach. There are, however, some aspects where this framework can be improved. First, we can introduce deadlock freedom and weak total correctness, which enable us to reason about termination of programs. Second, we can provide VCG tactics to achieve a higher level of automation. Currently, the language supports annotation to provide loop invariant, but the soundness of annotated rules is yet to be proven. Third, it is also desirable to have completeness of the proof system to introduce properties proven at the language and semantics levels. The complexity of proving completeness make us to consider this as future work. Finally, the current proof system do not include a rule for recursive procedure calls, but our framework can be easily extended to support it, with minimal modifications on the rules already proven.