| theme | academic |
|---|---|
| layout | cover |
| coverAuthor | Matteo Brambilla - Paolo Cerutti - Carlo Chiodaroli |
| css | unocss |
| download | true |
| highlighter | shiki |
| hideInToc | true |
Promela-based tool for analyzing the logical consistency of concurrent systems, specifically data communication protocols
- automatically verify whether
M ⊨ φholds, whereMis a (finite-state) model of a system and propertyφis stated in some formal notation. - Can be used via command line or via iSpin
With SPIN one may check the following type of properties:
- deadlocks (invalid end-states)
- assertions
- unreachable code
- LTL formulae
- safety and liveness properties
- non-progress cycles (live-locks)
- acceptance cycles
SPIN uses a depth first search algorithm (DFS) to generate and explore the complete state space.
In simple terms:
procedure dfs(s: state)
if error(s) then report error
add s to stateSpace //stateSpace is an hashtable of states
foreach successor t of s do
if t not in stateSpace then dfs(t)
end dfsConstruction and error checking happen at the same time, this allows SPIN to be an on-the-fly model checker.
SPIN then builds a Büchi automaton from the negated LTL formula and from the state space, and checks whether the intersection of the two is empty.
If so, the property holds.
SPIN also has the possibility to use breath first search (BFS) to explore the state space which generates shorter counterexamples but increases the memory usage.
SPIN has several optimizations to make verification more efficient and more effective:
- partial order reduction
- the validity of a property does not depend on the order in which independent executed events are interleaved so it is sufficient to check one of the possible interleaving
- bitstate hashing
- instead of storing the whole state, only one bit of memory is used to store a reachable state
- state vector compression
- instead of storing the whole state, a compressed version of the state is stored
- minimization of the Büchi automaton
- states are stored in a deterministic automaton that changes dynamically during the verification process (very memory efficient but really slow)
- dataflow analysis
- SPIN can detect when a variable is not used anymore and remove it from the state space
layout: center class: "text-center"
- 5 basic types, default initialized to
0
bit turn=1; /*[0..1]*/ bool flag=true; /*[0..1]*/
byte counter; /*[0..255]*/
short s; /*16-bit signed*/ int msg; /*32-bit signed*/- Array
byte a[10]; /*array of 10 bytes*/- Create enum with
mtype
mtype = {A, B, C};
mtype d;
mtype:fruits = {apple, banana, pear};- Variable size integer could be declared with
unsigned
unsigned x : 5 = 10- More complex type defined using
typedef
typedef My_array {
byte a[10];
int b;
}
My_array a; /*declaration of new type*/
a.b = 10; /*access to field*/- Variables could be used in assignments or expressions
a = 10;
a = a + 1;
a >= 10A statement is a single instruction that can be executed by the program
- Executable if it can be executed immediately
- Assignments are always executable
- Blocked if it cannot
- Expressions are blocked if they evaluate to
0
- Expressions are blocked if they evaluate to
2 < 3 //always executable
x < 27 //only executable if value of x is smaller 27
3 + x //executable if x is not equal to –3skipis always executable, it just cause the process counter to go one step forwardassert(<expr>)is always executable, but if<expr>evaluates to0the program stops with an exception
Processes are the main component of a Promela program. They are the basic behavioral unit and are executed concurrently.
proctype Foo() {
...
}- They are defined using
proctypekeyword - They communicate with others using global variables and channels
- They are created using
run, which returns the process id- They could be created at any moment by other processes
- They start immediately after the run statement
- They could be started at creation using
active, eventually with a quantity to spawn more process at the same timeactive[2] proctype Bar() { ... }
/* A "Hello World" Promela model for SPIN. */
active proctype Hello() {
printf("Hello process, my pid is: %d\n", _pid);
}
init {
int lastpid;
printf("init process, my pid is: %d\n", _pid);
lastpid = run Hello();
printf("last pid was: %d\n", lastpid);
}initis the first process to be executedprintfis a built-in function to print on the console_pidis a built-in variable that contains the process id
layout: center class: "text-center"
if
:: <expr> -> <statement>; ...
:: <expr> -> <statement>; ...
:: else -> <statement>; ...
fi;- execute the statement of an executable
<expr>
- the
elsebranch is executed if all the other are blocked
- if no
<expr>is executable andelsebranch not provided, the process is blocked
The -> operator is an alias for ; used to separate the <expr> from the <statement>
- Useful to model non-deterministic branching
if
:: skip -> x = 0;
:: skip -> x = 1;
:: skip -> x = 2;
fi; do
:: <expr> -> <statement>; ...
:: <expr> -> <statement>; ...
:: <expr> -> break ...
:: else -> <statement>; ...
od;- works like
if, but at the end of the statements list it restarts from the choice point - if no
<expr>is executable the process is blocked;elsebranch could be provided - the
breakstatement is used to exit the loop
The communication between processes is done using channels. A channel is a FIFO queue of messages.
chan c = [10] of {byte, int};- The channel must have a bounded size
- The message structure supported by the channel is defined in the
ofclause - A channel could be used for two-way communication
- Using the same channel for communication between multiple process could be tricky
- use
xrto assert exclusive reading - use
xsto assert exclusive writing
- use
A message is sent to a channel using the ! operator
channel! <expr>, <expr> ...;The action is executable only if the channel is not full
<template #right>A message is received from a channel using the ? operator
channel? <var>, <var> ...;The action is executable only if the channel is not empty
❗The <expr> type must match the message type defined in the of clause
If one constant or enum value is provided, instead of a variable, the statement is executable only if the message attributes match the provided constants
It is possible to use channels as synchronization mechanism between processes. The channel size must be 0
- The sender is blocked until the receiver is ready to receive the message
chan c = [0] of {bit, byte};
proctype Sender() {
c!1, 3+4;
}
proctype Receiver() {
byte x;
c?1, x;
}
- Only when both processes are ready these statements are considered executable
All the single statements are atomic, but it is possible to group them in a block to make them atomic together
atomic {
<statement>; ...
<statement>; ...
}- It is executable if the first statement is executable
- If one of the following statements isn't executable, then process is temporarily suspended
No pure atomicity!
Real total atomicity is obtained with the d_step statement
d_step {
<statement>; ...
<statement>; ...
}- It is executable if the first statement is executable
- If one of the following statements is not executable, then generates a runtime error
Promela is a functional language, so it does not have a time model.
However, process often require clock or timeout to resend data
This is possible to model using the timeout statement
active proctype Receiver()
{
bit recvbit;
do
:: toR ? MSG, recvbit -> toS ! ACK, recvbit;
:: timeout -> toS ! ACK, recvbit;
od
}timeoutis executable if no other statements are executabletimeoutis used to escape from deadlocks
Goto is used as unconditional jump to a label
goto <label>;labelis an identifier that precedes a statementgotojump to the label and executes the statement- Useful to model communication protocols
wait_ack:
if
:: B?ACK -> ab=1-ab ; goto success
:: ChunkTimeout?SHAKE ->
if
:: (rc < MAX) -> rc++;
F!(i==1),(i==n),ab,d[i];
goto wait_ack
:: (rc >= MAX) -> goto error
fi
fi ;```promela { ...} unless {; ...}; ```
- Execute
statementuntilguardis true, then executeerr_stat - Useful to model exception handling
```promela ltl ::= opd | ( ltl ) | ltl binop ltl | unop ltl ```
-
opdis an operand -
unopis a unary operator-
[]is the globally operator$\square$ -
<>is the eventually operator$\diamond$ -
!is the negation operator$\neg$
-
```promela ltl ::= opd | ( ltl ) | ltl binop ltl | unop ltl ```
opdis an operandunopis a unary operatorbinopis a binary operatorUis the temporal operator strong untilWis the temporal operator weak until (only when used in inline formula)Vis the dual of U:(p V q)means!(!p U !q)&&or/\is the logical and operator||or\/is the logical or operator->is the logical implication operator<->is the logical equivalence operator
A ltl property should be declared in global scope, like a global variable
ltl [name] { formula }; ltl p { [] b }; // b always true
bool b = true;
active proctype main() {
printf("hello world!\n");
b = false;
}
layout: center class: "text-center"
Starting from Spin v4.0 it is possible to use C code in Promela with some limitations
It's primarily used by automatic verification tool, like Modex that we will see later, to model complex statement <template #left>
- declaration of complex type or struct
- atomic expression
- atomic expression executable only when evaluate to non zero value
c_expr must contain only one statement without any side effect because could be executed multiple times
- C code could access global identifier declared in Promela using
nowfollowed by a dot because it refers to the state vector
c_code { now.<identifier> = <expr>; }- C code could access local identifier declared inside process using
Pfollowed by the name of theproctypeand the pointer arrow because it refers to the state vector of the process
c_code { P<proctypeName>-><identifier> = <expr>; }- Promela code could access C identifiers declared with
c_declafter they have been inserted into the state vector. This approach substitute the usage ofc_track
c_state "<type> <identifier>" "Global|Local"; $spin -a model.pml
$gcc -o pan pan.c
$./pan