Sep 6, 2005 - gramming languages (see JPF [16] and SLAM [2, 1]). These ... (i.e. Corba ORB implementations). ...... //displacing the window for (i=0 ...
Model Checking Software with Well-Defined APIs: The Socket Case P. de la Camara, ´ M. M. Gallardo, P. Merino, D. Sanan ´ Dpto. de Lenguajes y Ciencias de la Computacion ´ University of Malaga, ´ Spain {camara,gallardo,pedro,sanan}@lcc.uma.es
ABSTRACT
concurrent and critical systems. Many commercial and academic projects are focused on exploiting the techniques and algorithms developed in the context of formal description techniques (FDTs) to the usual implementation languages. In that context, there are two main approaches to deal with this adaptation. The first one is the “model-extraction” one, that consists in translating the program into an FDT for an existing model checker (see Feaver [10], JPF1 [11] and Bandera [13]). Translation usually implies a reduction of the details (abstraction) in such a way that the final model only contains relevant aspects oriented to a given set of properties. Ensuring that the abstraction is correct with regard to the properties is a critical aspect. A clear point in favor of this approach is that the effort to obtain tools is reduced, and it is possible to take advantage of new optimizations in popular model checkers like spin[8, 9]. The second approach consists in implementing “languagespecific tools” which are designed to analyze particular programming languages (see JPF [16] and SLAM [2, 1]). These tools remove the translation and make it easier to go back to code from the errors. However, they need a considerable amount of development work. Some of these tools, like Verisoft [7], only search for a limited number of executions. One open problem in both approaches is the presence of external functions in the software to be analyzed. For instance, many potential unreliable applications are constructed on top of the mature TCP/IP transport level using a Berkley-like Socket interface. These applications range from proprietary software to widely used Internet services like FTP, SMTP, Telnet or standard Middleware support (i.e. Corba ORB implementations). Furthermore, in order to obtain greater efficiency, these applications are implemented with C or C++ languages and make use of functionality external to the language for concurrency management, input/output, time management, etc. The result is software with many calls to external functions that should be “interpreted” (“abstracted”) when making model checking. In this paper, we focus on correct model-extraction methods to verify concurrent software with well defined-APIs. Our aim is to verify concurrent C applications that make extensive use of operating system facilities through system calls. We propose to model the behavior of the operating system API and to obtain a correct abstraction of the software that makes use of this API. Both components, the model of API and the model of software, are then verified with existing model checkers. Following this method, we deal with three specific problems: (1) We need to take into account a formal semantics of the API offered by the
The application of model checking technology to real software seems to be a promising and realistic approach to increase its quality. There are some successful examples of tools for this purpose, mainly working with self-contained programs. However, verifying software that uses external functionality provided by the operating system via APIs is currently a challenging trend. In this paper, we give a method for using the tool spin to verify distributed software systems that use the API Socket and the network protocol stack TCP/IP for communications. Our approach consists in building a model of the underlying operating system to be joined with the original C code in order to obtain the input for the model checker. We define and use a formal semantics of the API to conduct the correct construction of models. The whole modelling process is transparent to the C programmer, because it is performed automatically and without special syntactic constraints in the input C code. Regarding verification, we consider optimization techniques suitable for this application domain, and we ensure that the system only reports potential (non-spurious) errors.
Categories and Subject Descriptors D.2.4 [Software Engineering]: Software/Program Verification—Formal Methods,Model Checking,Reliability
General Terms Reliability, Verification
Keywords Model Checking Software, SPIN
1.
INTRODUCTION
Adapting model checking techniques to programming languages has become a promising way to increase the quality of
Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. To copy otherwise, to republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. FMICS’05, September 5–6, 2005, Lisbon, Portugal. Copyright 2005 ACM 1-59593-148-1/05/0009 ...$5.00.
17
// Client process int main() { struct hostent *ptrh; struct protoent *ptrp; struct sockaddr_in sad; int sd; int port; char *host; ................. memset((char *)&sad,0,sizeof(sad)); sad.sin_family = AF_INET; sad.sin_port = htons((u_short)port); inet_aton(&sad.sin_addr,"127.0.0.1"); if ( ((int)(ptrp = getprotobyname("tcp"))) == 0) { fprintf(stderr, "bad protocol number"); exit(1); } sd = socket(PF_INET, SOCK_STREAM, ptrp->p_proto); if (sd < 0) { fprintf(stderr, "socket creation failed\n"); exit(1); } if (connect(sd, (struct sockaddr *)&sad, sizeof(sad)) < 0) { fprintf(stderr,"connect failed\n"); exit(1); } read(0,cadena,sizeof(cadena)); n = write(sd,cadena,strlen(cadena)); n = read(sd, buf, sizeof(buf)); while (n > 0) { write(1,buf,n); read(0,cadena,sizeof(cadena)); n = write(sd,cadena,strlen(cadena)); n = read(sd, buf, sizeof(buf), 0); } close(sd); exit(0); }
underlying operating system, in such a way that we can reason about the correctness of the model; (2) It is necessary to keep the constraints in the input language reduced to a minimum. For instance, it is important to offer some level of dynamic memory management; (3) It is necessary to make a model compatible with existing optimization techniques in the model checker, and even, to implement new methods. We have considered these problems in order to verify distributed software written in C and using the socket API. We have defined an operational semantics for the socket API, and we have used it as a reference to model the API in order to have a closed model to be verified with spin (version 4). The construction of the model for spin is automatically produced with a new tool. We have satisfactorily solved specific difficulties such as: a) preventing state space explosion due to modelling communication buffers, b) modelling realistic errors such as failing connections, c) modelling the user interaction or d) parsing different kinds of input syntax, thus normalizing the user code. The result is a tool that can process free style C code in order to detect errors like deadlock, assertion violations, non reachable code and LTL properties (the errors that can be analyzed with spin). The paper is organized as follows. Section 2 gives an overview of the socket API and the usual programming errors that we try to debug. Section 3 presents the main aspects of our formal operational semantics for this API. Section 4 describes the mapping scheme to construct the input spin model from the original C. The new tool SocketMC is presented in Section 5. Finally, we provide conclusions and future work.
2.
// Server process int main() { struct hostent *ptrh; struct protoent *ptrp; struct sockaddr_in sad; struct sockaddr_in cad; int sd, sd2; ................. memset((char *)&sad,0,sizeof(sad)); sad.sin_family = AF_INET; sad.sin_addr.s_addr = INADDR_ANY; sad.sin_port = htons((u_short)port); if ( ((int)(ptrp = getprotobyname("tcp"))) == 0) { fprintf(stderr, "cannot map \"tcp\" to protocol number"); exit(1); } sd = socket(PF_INET, SOCK_STREAM, ptrp->p_proto); if (sd < 0) { fprintf(stderr, "socket creation failed\n"); exit(1); } if (bind(sd, (struct sockaddr *)&sad, sizeof(sad)) < 0) { fprintf(stderr,"bind failed\n"); exit(1); } if (listen(sd, QLEN) < 0) { fprintf(stderr,"listen failed\n"); exit(1); } alen = sizeof(cad); if ( (sd2=accept(sd, (struct sockaddr *)&cad, &alen)) < 0) { fprintf(stderr, "accept failed\n"); exit(1); } do { c=read(sd2,buf,sizeof(buf)); c=write(sd2,buf,cReceived); } while(1); close(sd2); }
PROGRAMMING WITH THE SOCKET API
The socket API comprises a number of functions to create reliable communication channels for processes and/or to transfer (reliable or unreliable) data. Table 1 summarizes the behavior of the usual functions to make use of TCP and UDP facilities in the context of Internet communications. Although these functions are key to writing clientserver applications with TCP/IP, real code contains many other auxiliary functions (to manage IP addresses and host names, to discover ports associated to services, etc). It is also very common to use the standard C library for string and memory management, etc., as shown in Figure 1. A key point to be considered when making abstraction of programs is the kind of properties to be verified, or the kind of errors to be discarded. The operating system facilities and the program itself should be abstracted in order to keep enough information to check the desired properties. The usual errors in socket-based software could be classified in three categories: 1. Errors in the API.- Improper use of the socket functions that makes some calls fail: erroneous sequences of calls, wrong parameters, uncontrolled use of resources (ports), etc. 2. Errors in auxiliary functions .- Wrong use of standard functions employed to manage data such as IP addresses (inet addr), ports (htons), services (getservicebyname) or protocols (getprotobyname). 3. Other programming errors .- Arithmetic errors, memory allocation or deallocation, input/output functions, etc.
Figure 1: Excerpt of the original C code for a client and a server
18
TCP
API function socket bind listen connect accept read write close select
Table 1: Socket primitives meaning create a socket reference associate a local port and IP address configure number of incoming calls active call to create connection passive connection reliable streaming reception reliable streaming sending remove connection parallel attention to several sockets
sendto recvfrom close
unreliable packet sending packet receiving remove connection
main parameters transport protocol port and IP address limit remote point local point socket, buffer socket, buffer socket sockets
UDP
the set of buffers; (5) pair represents the opposite end point of the stream. We assume that function nbuf :→ Buffer creates a new buffer structure, and that nfull : Buffer → {f alse, true} checks if the buffer is not full. Let End P oints be the set of tuples defined above and consider the set Idep of identifiers of the elements of End P oints. We assume that function nep :→ Idep creates a new end point (⊥, created, ⊥, ⊥, ⊥) and returns its identifier. In the sequel, we will not distinguish between end points and their identifiers. Symbol ⊥ means that the corresponding component is not initialized. In order to simplify the notation, we will access the state and pair of a given end point k using state(k), pair(k). By definition, a buffer b in an end point k can be read by the process that may access k and it can be written by the process pair(k). Variables and streams handled by OS constitute the environment of the SBS system. Formally, an environment is a partial function e : (IdP ∪Idep ) → ((V ar → V alue)∪End P oints∪{null}) where function V ar → V alue is supposed to associate each process variable with its current value. If i ∈ IdP then e(i) ∈ V ar → V alue is a partial function representing the local variables of the process instance i. If v ∈ V ar is a variable local to i, its value is accessed using e(i)(v). Function e(i) is partial because process i cannot access variables belonging to other processes. On the other hand, if k ∈ Idep is the identifier of a particular end point, then e(k) ∈ End P oints represents the corresponding tuple. Process variables may store end point identifiers, that is, Idep ⊆ V alue. Thus, e(i)(v) = k ∈ Idep means that v points to an end point structure that may be accessed through e(k). If k ∈ Idep , e(k) = null means that the identifier k does not currently point to any valid end point. Processes may access and modify environments in two different ways: (1) pure C instructions executed by the C process instance i ∈ IdP only updates variables belonging to i, and (2) a system call executed by a C process i ∈ IdP updates both variables belonging to i and End P oints. Given a particular SBS composed of a set of C processes and an environment e, the operational semantics of the system is given by formalizing the behavior of the two kinds of instructions Cinst and scall. In any case, we do not modify the semantics of Cinst when making the translation into promela for spin.
Some of the previous errors can be corrected with the help of the compiler. However, it is desirable to provide some automatic support to discover as many errors as possible during verification. SocketMC is mainly oriented to check the first and second categories of errors.
3.
packet, remote receiver packet, origin
A FORMALIZATION OF THE SOCKET API
The abstraction approach proposed is actually based on constructing an implementation of the operating system. So the correctness of the approach can be obtained by analyzing the relation between this implementation and the standard description of the API functions. For this purpose, we give a formal operational semantics to the original API and we ensure that our implementation matches the semantics. In this section, we give some details about the semantics which defines the interleaving produced by a system composed of several C processes that communicate with the API socket.
3.1 Definitions A socket-based software (SBS) is a system composed of a number of C processes executing on a distributed system that communicate using the API socket and the network protocol stack TCP/IP. We assume that each C process is identified by an element of the set IdP . Processes execute two kinds of instructions: pure C instructions (Cinst) and system calls (scall). In order to give an operational semantics to the SBS, and particularly to scall, we consider a unique operating system (OS) grouping all the computational and communicating facilities in the distributed system. OS is responsible for hosting all process variables (from now on, denoted by V ar) and all socket-based communication channels (streams). A stream is a reliable (TCP-based) bidirectional communicating channel between two C processes. It is composed of a pair of end points, each one defined as a tuple name, state, port, buffer , pair, where (1) name is a unique identifier for the end point in the SBS; (2) state is the status of the local side of a stream, an element of States = {cre, bou, con, lis}, these constants meaning created, bound, connected, and listening; (3) port is a unique combination of Internet address and port. We denote with P ort the set of all available ports, and assume that function nport :→ P ort always returns an available port; (4) buffer is a unidirectional buffer to implement data transfer. We denote with Buffer
19
where each ci represents the program counter of the C process i. Given a program counter ci , we suppose that I(ci ) gives us the corresponding instruction in process i, and that function next(ci ) returns the program counter of the executable instruction in process i following I(ci ). We are considering that the flow control of processes is handled by this function, that is, the semantics of loops (for, while and dowhile), selections (if, ifelseif) and unconditional jumps (goto) is included in next.1 Functions I and next are similar to those defined in [5]. Rule API1 defines how the socket functions modify configurations, except for the synchronous execution of connect and accept. Set scalli represents the socket API functions indexed with the process identifier i, and it is used to exclude the synchronous execution of connect and accept which is defined in rule API2. Finally, rule Cinst introduces the interleaved semantics of pure C sentences.
In the next section, we summarize a formal semantics of SBS focusing on scalls.
3.2 Semantic rules for SBS The semantic rules given in Figures 2 and 3 formalize single execution steps in C processes in the SBS. We have defined the semantics using two different levels. The first one which focuses on process instructions includes two kind of rules: (a) the rules defining C instructions (− →c ) (not developed in this paper), and (b) the rules defining the semantics of the API socket (− →s ). The second (the top ) level (−→) defines the interleaving behavior of processes in execution. Figure 2 shows the semantics of the functions of the API scalli socket. Rules like e− −−−→s e are given as rules of labelled transition system, where scalli is the system call function executed (i being the process instance that executes it), e is the environment before executing the instruction, and e is the new environment produced. In order to simplify the presentation of rules, we denote environments as tuples e1 , · · · , en , eep where {1, · · · , n} are the identifiers of the C processes in execution, ∀i ∈ {1, · · · , n}.ei = e(i) and eep = e|Idep . Since the number of processes in execution may vary, the number of ei components is not fixed. Notation ei [k/v] and eep [name, state, port, buf , pair/k] is used to construct new environments. Thus, environment · · · , ei [k/v], · · · , eep [name, state, port, buf , pair/k] is equal to e except for variable v in process i that is now bound to variable k, which is bound to name, state, port, buf , pair. Creation and elimination of a socket (and end point) are respectively modelled by rules socket and close. Note that socket uses function nep to create a new end point structure pointed by k and gives the pointer to the process variable. Rules bind and listen model the transition to states bound (bou) and listening (lis) in the process that will accept connections. Rule con-acc defines how to create a TCP connection between two end points. Note that the new buffers included in the end point structures seem to be unconnected. However, as they are defined to be unidirectional, they are implicitly connected by the function pair. The mechanism for reading and writing is shown in rules read and write. Notation d.b represents a buffer with at least one data d at its head to be read. Similarly, b.d indicates the buffer b with the new data d at its tail. The formalization of select is a more complex case. This call only checks whether other system calls can be successfully executed, and it updates a variable (denoted by n in the rules) in the calling process to register the number (and names) of sockets that can be read or connected. We now omit the resulting list of active sockets, and only return a number. Note that select is defined in a recursive way using several rules. Rules select-2 and select-3 are used to check available data for reading, and available input connections, respectively. The next variant (select-4) allows us to check further socket descriptors when the first one is inactive. Finally, rule select-1 is used to stop the recursion. Note that condition ei (n) > 0 in select-4 ensures that select blocks when no socket is active. As we are only interested in defining the API socket, we assume that rules for each C instruction have already been defined. Figure 3 shows the semantics of the interleaved execution of processes. In order to construct the high level semantic rules, we define system configurations as pairs c1 || · · · ||cn , e
3.3 Semantics for errors The semantic rules in Figures 2 and 3 provide the basic structure where it is possible to include new API functions and to specify more precise conditions for errors. We omit more details to make the approach readable. However, in practise, we have considered most of the realistic errors during the construction of the model for the API, and we have used the operational semantics produced by the semantic rules given above to check our model for spin. It is worth noting that we explicitly consider two different kinds of errors. The first one corresponds to real errors in the user programs due to scenarios like reading from (or writing to) a disconnected socket, binding an already bound socket, reading after a non successful select() call, etc. The second kind of errors are due to the simplifications made when constructing the model such as blocking a write() operation due to buffer overflow, failing when creating a new socket due to a limited number of available descriptors, etc. In general, the second category corresponds to errors due to the implementation of the API and they can be solved in other implementations. In any case, the user will always obtain the precise information about the origin of the errors.
4. MODELLING AND VERIFICATION OF SOCKET-BASED SOFTWARE WITH SPIN The idea of performing software model checking by modelling the well-defined APIs can be implemented with different model checking tools. One particularly interesting case is the use of the well-known tool spin, because it supports C code processing. In this section, we explain how to implement our approach with spin.
4.1 Promela and C promela [8, 9] is a non-deterministic language that borrows some concepts and syntax elements from Dijkstra’s guarded command language, Hoare’s CSP language and C programming language. A promela model is composed of a finite set of processes that are executed concurrently. Processes may share global variables or channels, it being possible to represent in the language both shared-memory and distributed-memory systems. Communication via chan1
Assuming that program counters are different for each process instance, we do not need to add index i to functions I and next.
20
(socket)
(bind) (listen)
(con-acc)
(read)
(write)
k=nep() socketi (v)
e−−−−−−→s ··· ,ei [k/v],··· ,eep [v,cre,⊥,⊥,⊥/k] ei (v)=k∈Idep ,eep (k)=v,cre,⊥,⊥,⊥,∃k ∈Idep .(port(k )=p,state(k )=cre) bindi (v,p)
e−−−−−−→s ··· ,eep [v,bou,p,⊥,⊥/k] ei (v)=k∈Idep ,eep (k)=v,bou,p,⊥,⊥ listeni (v)
e−−−−−−→s ··· ,eep [v,lis,p,⊥,⊥/k] ei (w)=k,eep (k)=w,lis,p,⊥,⊥,ej (v)=k ,eep (k )=v,cre,⊥,⊥,⊥,p =nport(),b=nbuf (),b =nbuf (),k =nep() conj (v,p)||acci (w,w )
e−−−−−−−−−−−−−−→s ··· ,ei [k /w ],··· ,eep [w ,con,p,b,k /k ,v,con,p ,b ,k /k ] ei (v)=k,eep (k)=v,con,p,d.b,k readi (v,r) s ··· ,ei [d/r],··· ,eep [v,con,p,b,k /k]
e−−−−−−→
ei (v)=k,eep (k)=v,con,p,b,k ,eep (k )=w,con,p ,b ,k,nfull(b ) writei (v,d)
e− −−−−−−→s ··· ,eep [w,con,p ,b .d,k/k ] close (v)
(close)
i e− −−−− −→s · · · , eep [null /k]
(select-1)
e−−−−−−→s · · · , ei [0/n], · · ·
(select-2)
selecti (n)
readi (v1 ,d)
selecti (n,v2 ,··· ,vn )
e−−−−−−−→s e −−−−−−−−−−−−→s e selecti (n,v1 ,··· ,vn )
e−−−−−−−−−−−−→s ··· ,e i [ei (n)+1/n],··· conj (v,p)||acci (v1 ,w )
(select-3)
selecti (n,v1 ,··· ,vn )
e−−−−−−−−−−−−→s ··· ,e i [ei (n)+1/n],··· readi (v1 ,n)
(select-4)
selecti (n,v2 ,··· ,vn )
e− −−−−−−−−−−−−−−→s e −−−−−−−−−−−−→s e
conj (v,p)||acci (v1 ,w )
selecti (n,v2 ,··· ,vn )
e−−−−−−−→s ,e− −−−−−−−−−−−−−−→s ,e−−−−−−−−−−−−→s e ,ei (n)>0 selecti (n,v1 ,··· ,vn )
e−−−−−−−−−−−−→s e
Figure 2: Semantic rules for scall
(API1) (API2) (Cinst)
I(ci )i
I(ci )i ∈scalli ,e−−−−→s e ···||ci ||··· ,e−→···||next(ci )||··· ,e
conj (v,p)||acci (w,w )
I(ci )=accepti (w,w ),I(cj )=connectj (v,p),e−−−−−−−−−−−−−−−→s e connectionij
···||ci ||···||cj ||··· ,e
I(ci )i
−→
···||next(ci )||···||next(cj )||··· ,e
I(ci )∈Cinst,e−−−−→c e ···||ci ||··· ,e−→···||next(ci )||··· ,e
Figure 3: Semantics for interleaved process execution
21
4.2 Modelling scheme
P rocess ::= [active[“[”NumberOfInstances“]”] ] proctype P rocessT ypeID {Decl; InstSeq} InstSeq ::= [l :] Inst{; [l :] Inst}∗ Inst ::= Basic|Jump|If |Do|Atomic|D Step Basic ::= BExp | Assign | Input | Output | Rendez Jump ::= goto l | break If ::= if BranchSeq fi Do ::= do BranchSeq od Atomic ::= atomic “{” InstSeq “}” D Step ::= d step “{” InstSeq “}” Input ::= ChannelId ? ExpSeq Output ::= ChannelId ! ExpSeq Rendez ::= Input | Output Branch ::= :: Inst BranchSeq ::= Branch{Branch}∗
Following [10], we have defined a mapping from the original C code to extended promela. The tool SocketMC automatically transforms each API call into promela code preserving the semantics given in Figure 2. Figure 5 shows part of a client code translated into promela. The corresponding whole code for client and server may be found in the Appendix. The mapping from the original code to promela is done replacing every process (every main function) with a proctype definition. Then, the body of every proctype is filled using the extensions for C code (c decl, c state, c expr and c code). This is done breaking the C code in the points where a call to API appears (see Figure 5 and Appendix). The final promela code preserves the sequential execution of every C block code between two system calls. Thus, when verifying the model, spin interleaves blocks and system calls as atomic sentences. The code of C blocks is not essentially modified, however the code for the system calls is replaced by an abstraction that meets the formal semantics defined in the previous section. For instance, the abstraction for the accept call is shown in Figure 6. Note that the API functions are implemented/abstracted using C, so they are always executed in one step. The whole implementation of the API for TCP functions is done with 2000 lines of (promela+ C) code. We now describe the main aspects of the transformation.
Figure 4: Part of the Promela Syntax nels may be synchronous, when it occurs through channels with size zero, or asynchronous, using channels as bounded buffers. In addition, processes may have local variables storing their local state. Figure 4 shows the syntax of a subset of promela. A process is declared by means of a proctype definition (a number of initial instances may be optionally specified). The process behavior is given by a sequence of possibly labeled sentences preceded by the declarative part. Basic sentences in promela are those that modify the local state of processes (or the global state of the system), that is, the assignments and the instructions for sending/receiving messages through channels. Boolean expressions, also defined as basic instructions in Figure 4, behave as guards that must be satisfied before continuing the execution. Instructions if and do in promela include guards selected in a non-deterministic manner. Statements atomic and d step define a sequence of instructions whose execution cannot be interleaved with instructions in other processes. The language also provides other statements, but we have omitted them here for the sake of simplicity. promela models usually represent reactive systems with a non-deterministic behavior. Instructions if and do manage the unpredictable behavior of the environment. In addition, as typically occurs in any concurrent system, the interleaving of instructions introduces another source of nondeterminism. Therefore, in general, a promela model defines a set of possible and correct executions, called execution traces/paths. spin produces these paths and checks absence of errors such us deadlocks. The latest version of spin (spin4) implements promela extensions to work with embedded C code. Construction c code allows the execution of any C code in a unreliable way: if the code fails, the model checker itself stops. Sentence c expr allows us to express guard conditions to be evaluated in a side-effects free manner. Constructions c decl and c state are used to declare C variables. With the first one, it is possible to include types and variable declarations which are hidden to spin. With the second one, c state, it is possible to declare variables and to decide on the kind of visibility for spin: to register variables during verification in the main spin structures (state vector, stack and explored stated), or to keep the variables hidden. Finally, c track allows the user to declare variables to be only stored into the spin stack (without registering into the state vector).
4.3 Control flow It is worth noting that the operational semantics described in the previous sections consider control flow included in the function to manage program counters (function next()). We transform the original C code to obtain a new version where loops, selections and jumps are replaced by the equivalent promela instructions do, if and goto as shown in Figure 5. The conditional expression in these kinds of statements is translated into a c expr construct. Because the c expr can not include sentences with side-effects, the conditional expression is substituted by a temporal variable storing its current expression value. The translation of loops needs additional code. In particular, it is necessary to use promela goto to implement break and continue C statements. This transformation is also necessary for functions that include system calls.
4.4 Modelling Data The current implementation keeps all the variables in the original C code, unless another specific action is required. Variables are packed in C structures and declared using the c decl primitive. In the promela code, variables will be referenced as fields inside this structure. This methodology preserves the behavior of the original C code. It is even possible to use many auxiliary C functions and macros as usual. For instance, it is possible to use functions for manipulating sockaddr in structures, like inet aton. Apart from the original C variables and structures, the new model also needs extra C and promela variables to implement the operating system functionality formalized in the previous section. The most relevant data structure for socket-based applications are the buffers employed between communicating nodes to implement streaming or packet transfer. These buffers are abstracted using C arrays which are accessed by the implementation of the system calls. Note
22
proctype eco_client(){ ... //Initializing socket structures //Create a socket SOCKET_MODEL(Peco_client->vLoc.model_check_rubbish_int,Peco_client->_pid, &(Peco_client->pLocal->errno_Model)); ... //Connect the socket to the specified server. CONNECT_MODEL(Peco_client->vLoc.model_check_rubbish_int,(Peco_client->vLoc.sd), (struct sockaddr*)&(Peco_client->vLoc.sad),Peco_client->_pid,&(Peco_client->pLocal->errno_Model)); //Repeatedly read data from socket and write in user’s screen. READ_MODEL(Peco_client->vLoc.model_check_rubbish_int,0,(Peco_client->vLoc.string), strlen((Peco_client->vLoc.string)), Peco_client->_pid,&(Peco_client->pLocal->errno_Model)); WRITE_MODEL(Peco_client->vLoc.model_check_rubbish_int,(Peco_client->vLoc.sd),(Peco_client->vLoc.string), strlen((Peco_client->vLoc.string)),Peco_client->_pid,&(Peco_client->pLocal->errno_Model)); c_code{ (Peco_client->vLoc.n)=Peco_client-> vLoc.model_check_rubbish_int;}; READ_MODEL(Peco_client->vLoc.model_check_rubbish_int,(Peco_client->vLoc.sd),(Peco_client->vLoc.buf),... c_code{ (Peco_client->vLoc.n)=Peco_client->vLoc.model_check_rubbish_int; }; c_code{ Peco_client->vLoc.model_check_rubbish_int=(Peco_client->vLoc.n)>0; }; do ::c_expr{Peco_client->vLoc.model_check_rubbish_int}-> { WRITE_MODEL(Peco_client->vLoc.model_check_rubbish_int,1,(Peco_client->vLoc.buf), (Peco_client->vLoc.n),Peco_client->_pid,&(Peco_client->pLocal->errno_Model)); .......... } ................. :: else -> break; od; ......
Figure 5: Excerpt of the translated code fragment from client
4.5 Accessing data for LTL properties
that this kind of representation offers more flexibility and efficiency than using promela channels for this purpose. Since we use byte stream-oriented sockets, buffers should be able to store large amounts of bytes, as real buffers do. This approach never produces systems with infinite states (note that promela models always have a finite number of states). However, storing the real whole messages is extremely inefficient and some kind of data abstraction is necessary. In fact, buffers only contain internal references to messages, instead of the messages themselves, thereby saving lots of memory. There is a function that produces a hash value for every message found in a write() call. For instance, let us consider the case of a typical application like FTP. The following scenario corresponds to the user authentication: LOCAL COMMANDS BY USER ftp (host) multics
username Doe password mumble
When specifying linear time temporal (LTL) properties, we may access C variables declared in the new promela constructions. However, variables in LTL formulas must be global (as promela native variables or as C hidden variables) to be analyzed by spin. The way that original C variables have been modelled in the system obliges us to adopt some mechanism for specifying new LTL formulas. Original C variables appearing in LTL formulas must be mapped into hidden variables to carry out the verification. In addition, it is necessary to use C code to check the values of the variables. For instance, consider the client-server example in Figure 1. We could verify that (in every execution) every data sent by the client is eventually followed by a data reception. Formally, this formula is defined as
ACTION INVOLVED Connect to host S, port L, establishing control connections. ’ represents commands from host U to host S, and ’ receive_data) #define send_data #define receive_data
c_expr {check_send() } c_expr {check_receive() }
Note that the propositions in the formula are defined using C functions that access to the C variables. For instance, function check send() returns true if the last sentence executed by the client was a successful call to the send() API function. This property is verified with the following results: State-vector 128 byte, depth reached 319, errors: 0 391 states, stored (425 visited) 335 states, matched 760 transitions (= visited+matched) 445 atomic steps
It is worth noting that both the number and the size of the states is comparable to a pure promela model doing a similar job.
23
#define ACCEPT_MODEL(ret, sd, sad, idp) \ atomic{ \ c_expr{ Accept_Block((sd), (sad), (idp)) }; \ c_code{ \ (ret) = Accept_Model((sd), (sad), (idp)); \ }; \ } int Accept_Model(int sockfd, struct sockaddr *addr, unsigned char pidSPIN) { int i; unsigned char fid, nFids, accFid; struct DataProcess* dProc; if (Accept_Error(sockfd,addr,pidSPIN)==1) return -1; if (Accept_Block(sockfd,addr,pidSPIN)==0) return -1; fid=(unsigned char)sockfd - FIDOFFSET; dProc=SearchDataProcces(pidSPIN, &nFids); //Gathering the next hanging connection accFid=dProc->Fidslist[fid].buffer[0]; //displacing the window for (i=0; iFidslist[fid].buffer[i]= dProc->Fidslist[fid].buffer[i+1]; } dProc->Fidslist[fid].buffer[(MAXWINDOWSIZE-1)]=NOTHING; // return the new socket return accFid+FIDOFFSET; }
Figure 6: Accept abstraction
4.6 Managing Dynamic structures Besides API socket, the current implementation also deals with dynamic memory allocation. To this end, we model the malloc() system call by using a section of memory as a heap. This heap is included as state information using the c track promela primitive. This way of managing dynamic memory is formally correct, but it increases the state space.
4.7 Optimization techniques Many structures employed to manage the API (IP addresses, messages composed of strings, etc.) have a considerable size. However, since these variables only take a limited number of values, we may replace them with a reference to a hash table, where their actual values are stored. This table stays out of the state representation, but it is coherent during the verification process. In order to deal with the state explosion problem, new techniques must be used in addition to those already included in spin. It is possible to take advantage of inherent symmetry in client side of client-server applications, using symmetry reduction. For instance, we often find scenarios where several equal client processes connect to a single server. Symmetry reduction correctly analyzes a single interleaving in the clients among all possible executions, which considerably reduces the number of reachable states.
5.
Figure 7: Architecture and GUI of SocketMC
ternal libraries different from those in the API model. Once the system processes have been specified, the translation itself is carried out. This task is performed by a Java application, C2Spn, which takes every C file from the GUI’s output and translates its main() function into a proctype() promela process. It also appends the fixed promela code to implement the API for sockets. The current version of C2Spn has been developed using JavaCC[12]. The GUI has been developed using TCL/TK 8.4. The tool has been tested with a realistic set of programs: student exercises in a course of communication software programming. Some of the programs were modified in minor points to be adapted to the current parser. SocketMC found many errors due to illegal uses of the system calls. More details will be available at http://www.lcc.uma.es/gisum/fmse/tools.
THE TOOL SOCKETMC
The tool SocketMC implements the functionality described in the previous sections. The architecture and GUI of the tool are shown in Figure 7. The conversion from C systems to promela involves two steps. Firstly, it is necessary to determine the processes intervening in the system. A graphical application allows the user to add files containing the C implementation of the processes, and gives the user the capacity to specify the variables and functions from ex-
24
6.
CONCLUSIONS AND FUTURE WORK
[3] T. Ball, A. Podelski, S. K. Rajamani. Boolean and Cartesian Abstractions for Model Checking C Programs, TACAS 01:Tools and Algorithms for Construction and Analysis of Systems, LNCS 2031, pages. 268-283. Springer-Verlag, 2001 [4] E. Clarke, D. Kroening, and F. Lerda. A tool for checking ANSI-C programs. In TACAS, volume 2988 of LNCS, pages 168-176. Springer, 2004. [5] M. M. Gallardo, P. Merino, E. Pimentel. A Generalized Semantics of Promela for Abstract Model Checking. Formal Aspects of Computing (2004) 16: 166-193 [6] M. M. Gallardo, J. Martinez, P. Merino, E. Pimentel. αSPIN: A Tool for Abstract Model Checking. Int. J. on Software Tools for Technology Transfer, 5 (2-3),pp. 165 - 184, 2004. [7] P. Godefroid. Model Checking for Programming Languages using Verisoft. In Proceedings of the 24th ACM Symposium on Principles of Programming Languages, 1997 [8] G. J. Holzmann. The Model Checker SPIN. IEEE Trans. on SE, 23(5), (1997). [9] G.J. Holzmann. SPIN Model Checker, The: Primer and Reference Manual. Addison Wesley, 2004. [10] G. J. Holzmann and M. Smith. Software model checking. Extracting verification models from source code. In Invited Paper. Proc. PSTV/FORTE99 Pulb. Kluwer, 1999 [11] K. Havelund, Thomas Pressburger. Model Checking Java Programs Using Java PathFinder. In International Journal on Software tools for Technology Transfer (STTT), 1999. [12] Java Compiler Compiler: The Java Parser Generator. Online documentation for version 0.7.1. Sun Microsystems. Available at [13] J. Corbett, M. Dwyer, J. Hatcliff, C. Pasareanu, Robby, S. Laubach, and H. Zheng. Bandera: Extracting Finitestate Models from Java Source Code. In Proc. of the 22nd Int. Conf. on Software Engineering, 2000, ACM Press. [14] M. Musuvathi, D. W. Park, A. Chou, D. R. Engler, D. L. Dill. CMC: A Pragmatic Approach to Model Checking Real Code. In Proc. of the Fifth Symposium on Operating Systems Design and Implementation, 2002. [15] S. D. Stoller. Model-Checking Multi-Threaded Distributed Java Programs. In Proc. 11th International Conference on Automated Deduction, LNAI-607 pages 748752, 1992. [16] W. Visser, K. Havelund, G. Brat, and S. Park. Model checking programs. In Proc. of the 15th Int. Conf. on Automated Software Engineering, pages 3-12. IEEE Comp. Society, 2000
We have developed a method to obtain spin models from socket-based C applications. These models contain most of the original C code and the abstraction of the system calls. Actually, the abstraction of these calls is fixed, and we only need to transform the specific code of the program to do model checking. There exist other model checking tools to verify code of distributed systems or software with well-defined APIs. For instance, Bandera and JPF are well suited to verify Java implementations of distributed systems. The algorithms proposed by Stoller [15] are also focused on carrying out state-space analysis of multi-threaded distributed systems. Regarding C oriented tools, the main current projects are SLAM, CMC and Feaver. SLAM is a model-checker specifically designed to verify that the behavior of a driver is secure with respect to the potential uses of the API that it offers. It includes boolean abstraction methods following [3]. CMC [14] and Feaver are focused on verifying C programs with an event-oriented scheme, so they are suitable to verify networking code. The first one is a model checker itself, and Feaver is a model extractor for spin. The main characteristics that SocketMC shares with these tools is the orientation to networking code. In particular, the use of C as the input language gives a wide area of application, because C is still the main language for “system-software” development. The main novelty of our work is the ability to include a correct model of the operating system support, for the considered API. Another important point is its integration with spin, a recognized model checker with powerful optimization methods. Current and future work is planned in two main lines. The first one consists in extending the API with other functions like UDP communication, process creation, multi-threading and signal handling. The second research line consists in dealing with efficiency. On the one hand, it is necessary to improve how to manage dynamic memory allocation (see [4]). On the other, we work on combining symmetry reduction techniques, data abstraction and static code analysis. In particular, we plan to integrate SocketMC with αspin [6]. Acknowledgments Work supported by projects TIC200204309-C02-02 and TIN2004-7943-C04-01.
7.
REFERENCES
[1] T. Ball, B. Cook, V. Levin, S.K. Rajamani: SLAM and Static Driver Verifier: Technology transfer of formal methods inside Microsoft. In: IFM. (2004) [2] T. Ball, R. Mjumdar, T. Millstein, and S. K. Rajamani. Automatic predicate abstraction of C programs. In Proceedings of the SIGPLAN ’01 Conference on Programming Language Design and Implementation, 2001
25
8.
APPENDIX
proctype eco_server(){ proctype eco_client(){ ... //Initializing socket structures //Create a socket SOCKET_MODEL(Peco_client->vLoc.model_check_rubbish_int, Peco_client->_pid,&(Peco_client->pLocal->errno_Model)); ... //Connect the socket to the specified server. CONNECT_MODEL(Peco_client->vLoc.model_check_rubbish_int, (Peco_client->vLoc.sd),(struct sockaddr *) &(Peco_client->vLoc.sad), Peco_client->_pid,&(Peco_client->pLocal->errno_Model)); //Repeatedly read data from socket and write in user’s screen. READ_MODEL(Peco_client->vLoc.model_check_rubbish_int,0, (Peco_client->vLoc.string),strlen((Peco_client->vLoc.string)), Peco_client->_pid,&(Peco_client->pLocal->errno_Model)); WRITE_MODEL(Peco_client->vLoc.model_check_rubbish_int, (Peco_client->vLoc.sd),(Peco_client->vLoc.string), strlen((Peco_client->vLoc.string)),Peco_client->_pid, &(Peco_client->pLocal->errno_Model));
... //Initializing socket structures //Socket creation SOCKET_MODEL(Peco_server->vLoc.model_check_rubbish_int, Peco_server->_pid,&(Peco_server->pLocal->errno_Model)); ... //Bind a local address to the socket BIND_MODEL(Peco_server->vLoc.model_check_rubbish_int, (Peco_server->vLoc.sd),(struct sockaddr*) &(Peco_server->vLoc.sad), Peco_server->_pid,&(Peco_server->pLocal->errno_Model)); ... //Specify the size of the request queue LISTEN_MODEL(Peco_server->vLoc.model_check_rubbish_int, (Peco_server->vLoc.sd),Peco_server->_pid, &(Peco_server->pLocal->errno_Model)); ... //Main server loop - accept and handle requests ACCEPT_MODEL(Peco_server->vLoc.model_check_rubbish_int, (Peco_server->vLoc.sd),Peco_server->_pid, &(Peco_server->pLocal->errno_Model)); c_code{Peco_server->vLoc.model_check_rubbish_int= ((Peco_server->vLoc.sd2)= Peco_server->vLoc.model_check_rubbish_int)vLoc.model_check_rubbish_int} -> { c_code{ 1; }; EXIT_MODEL(Peco_server->_pid);} ::else->; fi; c_code{Peco_server->vLoc.model_check_rubbish_int=1;}; do ::c_expr{Peco_server->vLoc.model_check_rubbish_int}-> {READ_MODEL(Peco_server->vLoc.model_check_rubbish_int, (Peco_server->vLoc.sd2),(Peco_server->vLoc.buf), sizeof((Peco_server->vLoc.buf)),Peco_server->_pid, &(Peco_server->pLocal->errno_Model)); c_code{ (Peco_server->vLoc.vGlob.cReceived)= Peco_server->vLoc.model_check_rubbish_int; }; WRITE_MODEL(Peco_server->vLoc.model_check_rubbish_int, (Peco_server->vLoc.sd2),(Peco_server->vLoc.buf), (Peco_server->vLoc.vGlob.cReceived),Peco_server->_pid, &(Peco_server->pLocal->errno_Model)); c_code{ (Peco_server->vLoc.vGlob.cReceived)= Peco_server->vLoc.model_check_rubbish_int; };} forlabelcontinueeco_server1: c_code{Peco_server->vLoc.model_check_rubbish_int=1;}; :: else -> break; od; forlabelendeco_server1: skip; CLOSE_MODEL(Peco_server->vLoc.model_check_rubbish_int, (Peco_server->vLoc.sd2),Peco_server->_pid, &(Peco_server->pLocal->errno_Model)); label_end_process: skip; c_code{Exit_Modelo( Peco_server->_pid);}; }
c_code{ (Peco_client->vLoc.n)=Peco_client-> vLoc.model_check_rubbish_int; }; READ_MODEL(Peco_client->vLoc.model_check_rubbish_int, (Peco_client->vLoc.sd),(Peco_client->vLoc.buf), sizeof((Peco_client->vLoc.buf)),Peco_client->_pid, &(Peco_client->pLocal->errno_Model)); c_code{(Peco_client->vLoc.n)=Peco_client-> vLoc.model_check_rubbish_int;}; c_code{Peco_client->vLoc.model_check_rubbish_int= (Peco_client->vLoc.n)>0;}; do :: c_expr{Peco_client->vLoc.model_check_rubbish_int}-> { WRITE_MODEL(Peco_client->vLoc.model_check_rubbish_int,1, (Peco_client->vLoc.buf),(Peco_client->vLoc.n), Peco_client->_pid,&(Peco_client->pLocal->errno_Model)); c_code{ 1; //printf in original code is eliminated }; READ_MODEL(Peco_client->vLoc.model_check_rubbish_int,0, (Peco_client->vLoc.string),strlen((Peco_client->vLoc.string)), Peco_client->_pid,&(Peco_client->pLocal->errno_Model)); WRITE_MODEL(Peco_client->vLoc.model_check_rubbish_int, (Peco_client->vLoc.sd),(Peco_client->vLoc.string), strlen((Peco_client->vLoc.string)),Peco_client->_pid, &(Peco_client->pLocal->errno_Model)); c_code{(Peco_client->vLoc.n)= Peco_client->vLoc.model_check_rubbish_int;}; READ_MODEL(Peco_client->vLoc.model_check_rubbish_int, (Peco_client->vLoc.sd),(Peco_client->vLoc.buf), sizeof((Peco_client->vLoc.buf)),Peco_client->_pid, &(Peco_client->pLocal->errno_Model)); c_code{(Peco_client->vLoc.n)= Peco_client->vLoc.model_check_rubbish_int;};} forlabelcontinueeco_client1: //evaluating the loop condition into the temporal variable c_code{Peco_client->vLoc.model_check_rubbish_int= (Peco_client->vLoc.n)>0;}; :: else -> break; od; forlabelendeco_client1: skip; CLOSE_MODEL(Peco_client->vLoc.model_check_rubbish_int, (Peco_client->vLoc.sd),Peco_client->_pid, &(Peco_client->pLocal->errno_Model)); EXIT_MODEL(Peco_client->_pid); label_end_process: skip; c_code{Exit_Model( Peco_client->_pid);}; }
26