Efficient Refinement Checking in VCC

We propose a methodology for carrying out refinement proofs across declarative abstract models and concrete implementations in C, using the VCC verification tool. The main idea is to first perform a systematic translation from the top-level abstract model to a ghost implementation in VCC. Subsequent refinement proofs between successively refined abstract models and between abstract and concrete implementations are carried out in VCC. We propose an efficient technique to carry out these refinement checks inVCC.We illustrate ourmethodologywith a case study in which we verify a simplified C implementation of an RTOS scheduler, with respect to its abstract Z specification. Overall, our methodology leads to efficient and automatic refinement proofs for complex systems that would typically be beyond the capability of tools such as Z/Eves or Rodin.


Introduction
Refinement-based techniques are a well-developed approach to proving functional correctness of software systems. In a correct-by-construction approach using step-wise refinement, one begins with an abstract specification of the system's functionality, say M 1 , and successively refines it via some intermediate models, to a concrete implementation, say P 2 in an imperative language. Similarly, in a post-facto proof of correctness, one begins with a concrete implementation P 2 , specifies its functionality abstractly in M 1 , and comes up with the intermediate models by simultaneously refining M 1 towards P 2 and abstracting P 2 towards M 1 . This is depicted in Fig.1(a). We note that it is convenient to have M 1 specified in an abstract modelling language such as Z [16] or Event-B [1], since this gives us a concise yet readable, and mathematically precise specification of the system's behaviour, which serves as a specification of functional behaviour for users and clients of the system.
Refinement-based proofs of functional correctness have several advantages over an approach of directly phrasing and proving pre and post conditions on methods. To begin with, refinement-based approaches help to break down assertions on complex programs using successive refinement steps, leading to more modular and transparent proofs. Secondly, they provide a useful framework for This work was partially supported by the Robert Bosch Center at IISc, Bangalore.
verifying clients of a library more efficiently. In principle, one could reason about assertions in a client program C that uses a concrete implementation of a library P 2 , by showing that C with a more abstract library M 1 satisfies the same assertions. This could lead to considerable reductions in the verification effort as reported in [9]. In a similar way, if one replaces a library implementation by a more efficient one, one does not have to reprove certain properties of its possibly numerous clients if one has shown that the new implementation refines the old one.
There are nevertheless a couple of key difficulties faced in carrying out refinement proofs between the successive models in a refinement-based approach, in our experience. The first is that performing a refinement proof between the abstract models (such as a proof that M 1 is refined by M 2 ), is challenging because the level of automation in tools such as Z/Eves [14] and Rodin [2] is inadequate, and requires non-trivial human effort and expertise in theorem proving to get the prover to discharge the proof obligations. The second hurdle we encounter is in showing the refinement between the abstract model M 2 and the imperative language model P 1 . The problem here is that there is no tool which understands both the modelling languages of M 2 and P 1 . One way of getting around this is to "import" the before-after-predicates (BAP's) from M 2 to P 1 , by using requires and ensures clauses that are equivalent to formulas in which the abstract state is existentially quantified away. But there are some disadvantages to this approach: (i) existential quantifications are difficult to handle for the theorem prover and can lead to excessive time requirement or can even cause the prover to run out of resources, and (ii) can be error-prone, and the equivalence should ideally be checked using a general-purpose theorem prover like Isabelle/HOL or PVS.
In this paper we propose a method of performing step-wise refinement and proving the ensuing refinement conditions, fully within the VCC toolset [6], with the aim of overcoming some of the hurdles described above. Continuing the example above, the idea is to first translate the high-level specification M 1 into a model G 1 in VCC's "ghost" modelling language. Next we refine G 1 to another ghost implementation G 2 in VCC, which will play the role of M 2 subsequently. How does this help us to get around the problems mentioned above? The first problem of proving refinement between the abstract models is alleviated as VCC is typically able to check the refinement between ghost models like G 1 and G 2 efficiently and automatically. The second problem of moving from an abstract model to an imperative implementation is also addressed because we now have both G 2 and P 1 in a language that VCC understands, and we can then proceed to phrase and check the refinement conditions (for instance by using a joint version of G 2 and P 1 together) within VCC.
Our contributions in this paper are the following. First, we provide a systematic and mechanizable translation procedure to translate specifications written in a subset of the Z modelling language to a ghost specification in VCC. The fragment of Z we target is chosen to cover the case study we describe next, and essentially comprises finite sequences and operations on them. There is an Fig. 1. (a) A typical refinement chain, with M1 and M2 being abstract models in a language like Z, and P1 and P2 being programs in a language like C. (b) The proposed translation and refinement chain, with G1 and G2 being "ghost" implementations in VCC. Dotted arrows denote the "refines" relationship.
inevitable blow-up of around 10x in the number of specification lines while going from Z to VCC, as VCC does not support many data-types (such as sequences) and operators that Z supports. While refining one ghost model to another (G 1 to G 2 ), the size of the model is not a problem: typically only a few aspects of the models change in each refinement step.
Secondly, we propose a two-step technique of phrasing the refinement check between ghost models and C programs in VCC that improves VCC's efficiency considerably. A naïve encoding of the refinement conditions can cause VCC to run out of memory due to the size of the model and complexity of the verification conditions. Using our two-step refinement check, VCC always terminates and leads to a reduction of over 90 % in the total time taken by a naïve check, when evaluated on our case-study.
The notion of refinement, theory and methodology for coming up with intermediate models used in this paper, are all based on the work in [7], where the functional correctness of a complex existing system-the FreeRTOS open-source real-time operating system [12]-was specified and verified. Experience with that case study, where we encountered the problems mentioned above, prompted us to explore these issues in a simpler setting. In this paper we use a simpler version of the FreeRTOS scheduler, which we built ourselves for this verification exercise. This scheduler, which we call Simp-Sched provides the same taskrelated API's as FreeRTOS (like vtaskCreate and vtaskDelay), but uses a task id (a number) instead of a full Task Control Block (TCB), and an array-based list library instead of the more complex circular doubly-linked xList library used in FreeRTOS. We begin with the Z specification of the scheduler API's that we used in [7], and apply the techniques described above to translate the initial model to VCC, and then carry out the refinement checks between successive models completely within the VCC platform. We carry out the refinement checks using different approaches explained in Sect. 4 and report on the comparative improvements we obtain over other approaches.

Preliminaries
In this section we introduce the notion of refinement we will use in this paper and a running example to illustrate some of the techniques we propose.
Consider a C implementation of a queue Abstract Data Type (ADT) (or library) shown in Fig. 2, whose functional correctness we want to reason about. This example is taken from [7]. The library uses an integer array A to store the elements of the queue. The variables beg and end denote positions in the array and the elements of the queue are stored starting from beg to end -1 in the array, wrapping around to the beginning of the array if necessary. The library provides the operations init, enq and deq to respectively initialize, enqueue, and dequeue elements from the list. The enq operation inserts the given element into the position end in the array, and the deq operation returns the element at the position beg in the array. Both operations update the len variable and increment the beg/end pointer modulo MAXLEN.
In a refinement-based approach we would begin by specifying the functionality of the queue abstractly. We could do this in the Z specification language for instance, as shown in Fig. 3. The model specifies the state of the ADT and how the operations update the state, using the convention that primed variables denote the post-state of the operation.
We now want to show that the queue implementation refines the abstract Z specification. Refinement notions are typically specified in terms of a simulation between the concrete and abstract models. The simulation is witnessed by an abstraction relation. In this case, a possible abstraction relation ρ we could use is roughly as follows: init . 3. A Z specification, z queue k , of a queue library which allows a maximum of k elements in the Queue. The notation ΔS for a Z schema S expands to the definition of S with an additional definition S representing the post state with primed field names.
The direction of simulation varies: a common notion of refinement used in the literature (for example in Event-B [1], and tools like Resolve [8], Dafny [11], and Jahob [17]), is to require the abstract to simulate the concrete. In this paper we use the notion from [7] where we require the concrete to simulate the abstract. We choose to use this notion as it gives us stronger verification guarantees. Nonetheless, the results we show in this paper are independent of the direction of simulation used, and apply for refinement notions with the other direction of simulation as well. We now briefly outline the notion of refinement used, and point the reader to [7] for more details.
An ADT type is a finite set N of operation names. Each operation name n in N has an associated input type I n and an output type O n , each of which is simply a set of values. We require that there is a special exceptional value denoted by e, which belongs to each output type O n ; and that the set of operations N includes a designated initialization operation called init. A (deterministic) ADT of type N is a structure of the form A = (Q, U, E, {op n } n∈N ) where Q is the set of states of the ADT, U ∈ Q is an arbitrary state in Q used as an uninitialized state, and E ∈ Q is an exceptional state. Each op n is a realisation of the operation n given by op n : (init) Let a ∈ I init and let (q a , b) and (q a , b ) be the resultant states and outputs after an init(a) operation in A and A respectively, with b = e. Then we require that b = b and (q a , q a ) ∈ ρ. (sim) For each n ∈ N , a ∈ I n , b ∈ O n , and p ∈ Q , with (p , p) ∈ ρ, whenever This is visualized in Fig. 4. − −−− → q denotes the fact that the ADT in state p can allow a call to operation n with argument a, return a value b, and transition to state q. We call this condition (RC).
In the rest of the paper we describe our contributions with this background in mind. Our aim is to carry out the refinement checks across abstract models and C implementations, fully within the VCC tool, in a way that gets around some of the problems with checking refinements outlined in the introduction. In Sect. 3 we explain how we can systematically translate a Z model to a ghost implementation in VCC. In Sect. 4 we explain different techniques for carrying out refinement checking in VCC, beginning with the natural approaches, followed by our more efficient two-step approach. We evaluate our techniques in a case study involving a simple RTOS scheduler called Simp-Sched, which we introduce in Sect. 5, and discuss the performance comparison in Sect. 6. Finally we conclude with some pointers to related work in Sect. 7.

Translating Z to VCC
The objective here is to translate an abstract Z model M into a ghost implementation G in VCC such that G M. The idea is to translate the state schema like that of z queue in Fig. 3, comprising fields and invariants, into a structure in VCC with corresponding fields and invariants. Similarly, for the operations as well.
We  Table 1 presents a look-up procedure for encoding various Z objects in VCC. If X is a set then the notation A X denotes an arbitrary subset of X. The Z objects are encoded in VCC in a way that facilitates easy proofs for the required verification conditions. This is crucial for scalability. Figure 5 shows excerpts from the VCC code obtained by translating the Z schema of Fig. 3. Table 1. Table showing the translation of Z objects to VCC objects. It gives a suitable encoding of Z objects in VCC which enables fast verification. If X is a set then the notation A X denotes an arbitrary subset of X. In this paper we present only those Z constructs that are used in the Z model of our case study in Sect. 5. Nevertheless other mathematical objects in Z can be handled in a similar way.

Phrasing Refinement Conditions in VCC
In this section we describe three ways to phrase the refinement condition (RC) of Sect. 2 as annotations in VCC. The first approach-which we call the "Direct-Import" approach-is useful when the abstract library is not available as a ghost model in VCC. Here one directly imports the abstract library as code level annotations in VCC. The second is the so-called "Combined" approach, which can be applied when the abstract library is available as a ghost implementation in VCC. Finally we describe our proposed "Two-Step" approach, which can again be applied when the abstract library is available as a ghost implementation, but which VCC discharges far more efficiently.
In each of these approaches we consider the case when the abstract model M is specified either as a Z specification or as a VCC ghost model, and the concrete model is given as an implementation in C, say P. For clarity, we focus here only on the (sim) condition of (RC).

Direct-Import Approach
This approach is applicable when the abstract model M is specified in a specification language like Z. The idea is to existentially quantify away the abstract state from a glued joint (abstract and concrete) state, and phrase this as pre/post conditions on the concrete methods. The resulting requires and ensures conditions are independent of the abstract state. Figure 6 shows a schematic for how one can apply the direct-import method in VCC. We use s and s to denote respectively the pre and post states of the abstract model, and t and t to represent the pre and post states of the concrete model. For an operation op, pre M op represents the precondition of op in library M. We use inv ρ to represent the abstraction relation which relates concrete and abstract states, and BAP to represent the predicates on pre and post states describing the transitions in the respective models.
Unfortunately this approach is not feasible in VCC as it is difficult for the theorem prover to handle the existential quantification. A possible way out is to transform the annotations to remove the existential quantification, and get an equivalent condition on the concrete state. For instance, for the queue example of Sect. 2, we could phrase the directly imported annotations by eliminating the existential quantification, as shown in Fig. 7 for the deq operation. The beforeafter predicates from the z queue model of Fig. 3 are phrased as annotations over data structures in the C implementation.
This approach has two disadvantages. Firstly, the manual transformation can be error prone and the equivalence should ideally be checked in a theorem prover like PVS or Isabelle/HOL. Secondly, the invariants and preconditions need to Fig. 6. Directly importing abstract library (M) using code level annotations in VCC.

Combined Approach
A second technique can be used to prove the refinement between two libraries when both are available as ghost or concrete implementations in VCC. The refinement condition (RC) of Sect. 2 can be phrased in VCC by using a combined function to update the instance of a joint structure which combines the fields of abstract and concrete libraries. The abstraction relation ρ can be specified as an invariant in the joint structure which we call a gluing invariant. To illustrate this on a simple example, consider an abstract ghost implementation G l of the queue library (Sect. 2) and another ghost implementation G k such that k ≥ l, where the subscript represents the maximum size of the queue. Figure 8 shows the joint structure to phrase the refinement condition between G l and G k and Fig. 9 shows the combined function to check the refinement between the abstract and concrete implementation of the deq operation.
Unfortunately when the concrete model is a C program, this approach could cause the prover to take lot of time or even run out of resources. In our opinion this is mainly due to the fact that a large number of extra annotations are required when reasoning about a joint (abstract and concrete) state that are both mutable. These annotations are required as loop invariants and as function contracts, to specify that each ghost object in the system is kept unmodified by a loop or function to modify the concrete data object. For instance, there should be a loop invariant in the combined function for updating the array A in the queue example of Fig. 2, which essentially says that each element in the abstract map (like lContent above) is kept unchanged. Similar predicates are required in the function contract if functions are used to update the concrete state. In our case study (Sect. 5), the number of such annotations required in a loop or function contract is about twice the number of annotations required in the proposed Two-Step approach to prove refinement conditions.

Two-Step Approach
We now propose an efficient approach, which we call the Two-Step approach, which overcomes some of the difficulties of the previous two approaches. The idea is to divide the refinement check into two steps. The first step is to prove the BAPs for the abstract and concrete functions separately by manually supplying the BAPs. The second step is to prove that the output states as defined by the BAPs satisfies the gluing invariant. The problem with the combined approach is avoided as in Step 1, we are interested in proving only the concrete BAP as the post condition of the concrete function and hence there is no need to specify the extra set of predicates in loops and concrete function contract to specify preservation of the abstract state. Figure 10 illustrates the two steps of our approach. Figure 11 shows the skeleton of the function in VCC to prove the abstract BAP for an library operation op. Here A and B represent the abstract and concrete libraries respectively, pre A represents the abstract precondition (like lLen = 0 for deq) and inv ρ   Fig. 11. Step 1 of the two-step approach for proving the abstract BAP. Fig. 12. Step 1 of the two-step approach for proving the concrete BAP.    Figure 12 shows the skeleton of the function in VCC to prove the concrete BAP. The annotation (decreases 0) says that the function terminates.
The second step of the Two-Step approach is checking the validity of the following implication and one can use a dummy function in VCC to check its validity.

Case Study: Simp-Sched
Here we describe our experiences with building specifications and a correctby-construction implementation of a software system. The system we chose to work with is the scheduler component of FreeRTOS, which is a popular realtime operating system (RTOS) that is widely used in the embedded systems community both in academic settings as well as in industry [3]. The FreeRTOS scheduler API provides operations to create, schedule, and remove tasks, as well as to delay and resume task operation (see Fig. 13 for a summary of task states). Application programmers can use these operations to implement application functionality using these tasks as units of behaviour. For our case study, we create a simplified version of the FreeRTOS scheduler, called Simp-Sched. Our new implementation maintains all key aspects of timing and scheduling. The simplification is based on two things: 1. Tasks in the FreeRTOS scheduler are maintained in a struct called a task control block, which includes pointers to function behaviour. In Simp-Sched, we simply use an integer task ID to represent the TCB of a task. 2. All task lists (ready, delayed, etc.) are maintained in a data structure called xlist, which is implemented as a circular doubly-linked list. In Simp-Sched, we replace this data structure with array implementations of the task lists.
All other aspects of the scheduler implementation are maintained. As such, the stock FreeRTOS scheduler implementation is a refinement of our Simp-Sched implementation. Given this, one of the key uses of our Simp-Sched implementation is use in a runtime monitor that can be used to identify potential scheduling inconsistencies and errors. Each API operation implementation in FreeRTOS can be instrumented to include a call to the corresponding operation in Simp-Sched, so that the two scheduler implementations are running in parallel.
The C implementation of Simp-Sched includes 769 lines of C code and 106 lines of comments [15]. The task lists are implemented as a separate library in which list is implemented using arrays in C. Figure 14 shows the components in the Simp-Sched implementation with interface operations.

Refinement Strategy for Simp-Sched
We now describe our methodology for constructing a correct C program from a mathematical specification of Simp-Sched by applying the refinement theory from [7]. The methodology involves five stages.
1. We start with a mathematical model in Z which we call M 1 capturing the high-level functionality of Simp-Sched. 2. We apply our mechanizable procedure explained in Sect. 3 to translate M 1 to a declarative model in VCC, which we call G 1 . Note that the translation guarantees that G 1 M 1 . 3. We then refine G 1 to a more concrete model G 2 in VCC to capture some machine level requirements. For example, the system clock is unbounded in G 1 , which is not directly realizable in the C language. In G 2 the clock value cycles in the interval [0, maxNumVal] where maxNumVal is the maximum value that an unsigned int in C can take. This change has another effect: the delayed tasks are maintained in a single delayed list in G 1 , which has to be broken into two lists in G 2 to cope with the bounding of the clock value. The refinement between G 2 and G 1 is verified using the combined approach explained in Sect. 4.2. 4. Next, we refine G 2 to P 1 where every data object except task lists in G 2 is implemented using executable objects and functions in C. The refinement between P 1 and G 2 is verified using the Two-Step approach explained in Sect. 4 5. Finally we refine P 1 to P 2 where the map-based abstract implementation of the list library (lMap) is replaced with an array-based list implementation (lArray). The refinement between P 2 and P 1 is verified using the Two-Step approach explained in Sect. 4.3. P 2 is a C program and we conclude using the transitivity and substitutability theorems from [7] that P 2 M 1 . The verification artifacts from this case study are available at [15] (Fig. 15).

Code Metrics and Human Effort Involved
We spent two human-months to complete this work. The code metrics are presented in the table in Fig. 16. Even though there are around 22,500 lines of code/annotations there is only a small modification required in successive refinements and hence the size of the initial model G 1 and L 1 model extracted from G 2 are the important parts deciding the human effort required. The models G 1 and L 1 contain 2,422 lines of annotation in VCC, which is about 3 times the size of the executable code in P 2 .

Performance Comparison
We report the time taken by VCC to prove the refinement conditions between different models in the case study. Table 2 shows the time taken under the three different approaches, namely the Direct-Import, Combined, and Two-Step approaches described in Sect. 4. Our Two-Step approach takes only 7.4 % of the total time taken by the Direct-Import approach. The time taken by the Combined approach is much longer than the time taken by the Direct-Import approach. This is because of the presence of the abstract objects, abstract invariants, gluing relation and abstract function body in addition to the overhead involved in the Direct-Import approach.

Related Work and Conclusion
As already mentioned, the work in this paper uses the foundation laid in [7], in terms of the theory of refinement and methodology used. There again, VCC is the main tool used for refinement checking: first for checking the refinement conditions between abstract Z models by translating them to VCC, and secondly for checking the refinements between the refined Z model and the concrete implementation. However the Z to VCC translation was partial, as they only needed to check the refinement between the changed API's. As a result the approach used for phrasing the refinement conditions for the abstract to imperative implementation step was the "Direct-Import" technique described in Sect. 4.1. In contrast, in this paper we have (a) given a systematic translation from Z to VCC and (b) proposed a two-step refinement check to phrase the refinement conditions in VCC that we show leads to significant improvements in our case study. VCC was used extensively in the Verisoft XT project [5] at Microsoft, where the goal was verification of Hyper-V hypervisor [10] and PikeOS [4] operating systems.