2014.10.14 david crocker

Download Report

Transcript 2014.10.14 david crocker

Can C++ be made as safe as SPARK?
David Crocker, Escher Technologies
Motiviation
• When developing software to very high integrity levels, testing is
insufficient to show the required integrity level has been reached, so
formal verification is typically requires as well
• The SPARK tool set is probably the most-used formal verification
system at the programming language level
• However, critical software is increasingly written in C and C++,
especially in the automotive sector
• Can development in C or C++ provide similar levels of software
integrity as development in SPARK?
Can C++ be made as safe as SPARK?
2
Bounded queue example
• Our example is a bounded queue of characters with fixed capacity
• I will show it might be implemented in SPARK Ada
• I will show how to implement a C++ equivalent that, like the SPARK
version:
– Provides a clean interface and hides the data
– Has a formal proof of correctness
Can C++ be made as safe as SPARK?
3
The package specification
-- File BoundedQueue.ads
package BoundedQueue with SPARK_Mode is
capacity: constant Integer := 64;
type Queue is private;
function empty(q: in Queue) return Boolean;
function full(q: in Queue) return Boolean;
procedure add(q: in out Queue; val: in Character) with
Pre => not full(q) ;
procedure remove(q: in out Queue; val: out Character) with
Pre => not empty(q) ;
procedure init(q: out Queue) with
Post => empty(q);
private
subtype StorageIndex is Integer range 0..capacity;
type Storage is array (StorageIndex) of Character;
type Queue is record
data: Storage;
nextIn: StorageIndex;
nextOut: StorageIndex;
end record;
end BoundedQueue;
Let’s try that in C…
// File BoundedQueue.h
#define CAPACITY (64u)
typedef struct
{
char data[CAPACITY + 1u];
unsigned int nextIn;
unsigned int nextOut;
} Queue;
// ranges from 0..CAPACITY
// ranges from 0..CAPACITY
bool empty(const Queue *q);
bool full(const Queue *q);
void add(Queue *q, char c);
// on entry the queue must not be full
char remove(Queue *q);
// on entry the queue must not be empty
void init(Queue *q);
// on return the queue is empty
Problem: lack of encapsulation
• The SPARK version declares Queue to be a private record type
• In the C version, the Queue structure is exposed
– which means that clients could reads/write its field directly
type Queue is private;
…
private
…
type Queue is record
data: Storage;
nextIn: StorageIndex;
nextOut: StorageIndex;
end record;
typedef struct
{
char data[CAPACITY + 1u];
unsigned int nextIn;
unsigned int nextOut;
} Queue;
• C has no adequate mechanism for data hiding
• Can we do better with C++?
Let’s try it in C++
// File BoundedQueue.hpp
const unsigned int capacity = 64u;
class Queue
{
public:
bool empty() const;
bool full() const;
void add(char c);
// on entry the queue must not be full
char remove();
// on entry the queue must not be empty
Queue();
// on return the queue is empty
private:
char data[capacity + 1u];
unsigned int nextIn;
unsigned int nextOut;
};
// ranges from 0..CAPACITY
// ranges from 0..CAPACITY
SPARK and C++ versions compared
• Both keep the data private
• Both provide empty and full functions, add and remove procedures
• Parameter passing:
– SPARK used in and out keywords to indicate direction
– C++ uses const and absence of const to indicate whether a parameter
passed by pointer is changed or not
• How to initialize a Queue
– In the SPARK version, you call the init procedure
– In the C++ version, the default constructor will be called automatically
• But the C++ version is still missing something…
Can C++ be made as safe as SPARK?
8
Function contracts!
• The SPARK version provides function contracts:
procedure add(q: in out Queue; val: in Character) with
Pre => not full(q) ;
procedure remove(q: in out Queue; val: out Character) with
Pre => not empty(q) ;
procedure init(q: out Queue) with
Post => empty(q);
• How to do this in C++?
• Maybe do it the way SPARK used to, in comments?
Can C++ be made as safe as SPARK?
9
Adding function contracts
const unsigned int capacity = 64u;
class Queue
{
public:
bool empty() const;
bool full() const;
void add(char c);
//# pre !full()
char remove();
//# pre !empty()
Queue();
//# post empty()
private:
char data[capacity + 1u];
unsigned int nextIn;
unsigned int nextOut;
};
// ranges from 0..CAPACITY
// ranges from 0..CAPACITY
Can C++ be made as safe as SPARK?
10
A nicer way of adding function contracts
• C++ provides a preprocessor
• The preprocessor can be used to define and expand macros
• So how about using macros:
#define pre(expression)
// nothing
…
void add(char c)
pre(!full());
• When the file is compiled, the compiler expands the pre(…) part to
nothing, so it gets ignored
• Text editors do syntax highlighting on macro calls, just as for code
Can C++ be made as safe as SPARK?
11
Adding function contracts
#include <ecv.h>
// for specification macro definitions
const unsigned int capacity = 64u;
class Queue
{
public:
bool empty() const;
bool full() const;
void add(char c)
pre(!full());
char remove()
pre(!empty());
Queue()
post(empty());
private:
char data[capacity + 1u];
unsigned int nextIn;
unsigned int nextOut;
};
// ranges from 0..CAPACITY
// ranges from 0..CAPACITY
Can C++ be made as safe as SPARK?
12
What’s still missing from the C++ version?
• The SPARK version uses range-constrained types:
subtype StorageIndex is Integer range 0..capacity;
type Storage is array (StorageIndex) of Character;
type Queue is record
data: Storage;
nextIn: StorageIndex;
nextOut: StorageIndex;
end record;
• It’s not essential to use range-constrained types in this example, but
they can help with verification
– e.g. by detecting out-of-range values earlier
• Why not add range-constrained types to C++?
Can C++ be made as safe as SPARK?
13
Adding range-constrained types to C++
• We could use a class template:
class ConstrainedInt<int minVal, int maxVal> {
public:
operator int() const { return val; }
ConstrainedInt(int arg) pre(arg >= minVal; arg <= maxVal) {
if (arg < minVal || arg > maxVal) {
throw ConstraintError(arg);
}
val = arg;
}
private:
int val;
}
ConstrainedInt<0, capacity> nextIn, nextOut;
• If we don’t need run-time checking, we can use annotations instead
Can C++ be made as safe as SPARK?
14
Extending the C++ typedef declaration
• C and C++ allow you to define synonyms for types:
typedef size_t StorageIndex;
• Let’s add constraints to typedef declarations:
#define invariant(expression)
// nothing
…
typedef size_t invariant(value <= capacity) StorageIndex;
• We also add the rule that a pointer to a constrained type is not
assignment-compatible with a pointer to any other type, even if the
constraint is “true”
Can C++ be made as safe as SPARK?
15
Using range-constrained types
#include <ecv.h>
// for specification macro definitions
const unsigned int capacity = 64u;
class Queue
{
public:
bool empty() const;
bool full() const;
void add(char c)
pre(!full());
char remove()
pre(!empty());
Queue()
post(empty());
private:
typedef unsigned int invariant(value <= capacity) StorageIndex;
char data[capacity + 1u];
StorageIndex nextIn;
StorageIndex nextOut;
};
What about the body?
Can C++ be made as safe as SPARK?
17
-- File BoundedQueue.adb
package body BoundedQueue with SPARK_Mode is
function empty(q: in Queue) return Boolean is
begin
return q.nextIn = q.nextOut
end;
function full(q: in Queue) return Boolean is
begin
return (q.nextIn + 1) mod (capacity + 1) = q.nextOut;
end;
procedure add(q: in out Queue; val: in Character) is
begin
q.data(q.nextIn) := val;
q.nextIn := (q.nextIn + 1) mod (capacity + 1);
end;
procedure remove(q: in out Queue; val: out Character) is
begin
val := q.data(q.nextOut);
q.nextOut := (q.nextOut + 1) mod (capacity + 1);
end;
procedure init(q: out Queue) is
begin
q.nextIn := 0; q.nextOut := 0;
q.data := (others => Character'Val(0));
end;
end BoundedQueue;
-- for SPARK
// File BoundedQueue.cpp
#include "BoundedQueue.hpp“
#include <cstring>
// for memset()
bool Queue::empty() const
{
return nextIn == nextOut;
}
bool Queue::full() const
{
return (nextIn + 1u) % (capacity + 1u) == nextOut;
}
void Queue::add(char c)
{
data[nextIn] = c;
nextIn = (nextIn + 1u) % (capacity + 1u);
}
char Queue::remove()
{
char temp = data[nextOut];
nextOut = (nextOut + 1u) % (capacity + 1u);
return temp;
}
Queue::Queue()
{
nextOut = 0;
nextIn = 0;
memset(data, 0, sizeof(data));
}
Verification in SPARK 2012 GPL edition
Can C++ be made as safe as SPARK?
20
Verification of the C++ version
…and so on until…
Can C++ be made as safe as SPARK?
21
Similarities and differences
• SPARK reports 5 VCs generated, 4 proved
– It appears to hide some “trivial” VCs, e.g. provable range checks
• Our tool for C++ reports 37 VCs generated, 36 proved
– The number of VCs doesn’t depend on whether they succeed or not
• Neither can prove that the init function or constructor yields an empty
queue
– Because we haven’t provided a specification for empty()
• We have proved that the program is valid in one sense
– “Exception freedom” for the Ada version
– “Absence of undefined or unspecified behaviour” for the C++ version
• But we haven’t proved that it behaves like a queue!
Can C++ be made as safe as SPARK?
22
How should a queue behave?
• Logically, a queue is a sequence of elements
– We add elements to one end and remove them from the other
Remove elements from head
Add elements to tail
• So we should specify the queue operations in terms of a sequence
• This calls for data refinement
– The abstract data is a sequence with varying numbers of elements
– The concrete data is a fixed length array and two indices into it
– A retrieve relation defines the relationship between abstract and concrete
data
Can C++ be made as safe as SPARK?
23
Expressing data refinement in C++
• We allow ghost functions to be declared
– A ghost function is for use in specifications only
• To express the retrieve relation, we declare a ghost function that
returns the abstract data
– Then we can write specifications in terms of calls to that function
• For the type of the abstract data, we can use _ecv_seq<char>
– This is a built-in ghost type that represents a sequence
– It supports the usual sequence operations including count(), head(), tail(),
take(n), drop(n), append(c) and concat(s)
– It supports quantification over the elements and a few higher order
functions (filter, map, left-fold, ...)
Can C++ be made as safe as SPARK?
24
Retrieve function for a circular buffer
• If nextIn >= nextOut, we want the elements in between them:
data.take(nextIn)
Queue contents are:
data.take(nextIn)
.drop(nextOut)
data
nextOut
nextIn
• Otherwise, we want the elements from nextOut to the end of the
buffer, followed by elements from the start of the buffer up to nextIn:
data.take(nextIn)
data.drop(nextOut)
Queue contents are:
data.drop(nextOut)
.concat(data.take(nextIn))
data
nextIn
nextOut
const unsigned int capacity = 64u;
class Queue
{
public:
// Retrieve function
ghost( ecv_seq<char> contents() const
returns( (nextIn >= nextOut)
? data.take(nextIn).drop(nextOut)
: data.drop(nextOut).concat(data.take(nextIn))
);
)
bool empty() const
returns(contents().count == 0);
bool full() const
returns(contents().count == capacity);
void add(char c)
pre(!full())
post(contents() == (old contents()).append(c));
char remove()
pre(!empty())
returns(contents().head())
post(contents() == (old contents()).tail());
Queue()
post(empty());
private:
…
Verification with data refinement
…and so on…
Can C++ be made as safe as SPARK?
27
Also in the paper…
• Weaknesses in the C++ language have to be mitigated
– We apply most MISRA-C:2008 rules
– We use annotations to strengthen the type system in respect of pointers
– We only support those C++ constructs that we believe are safe to use
and have formalised
– We add further safety rules, e.g. to restrict calls to overloaded functions
• Single inheritance with dynamic binding
– We prove subtype compatibility (Liskov Substitution Principle) as required
by DO-332 objective OO-6.7.2
• C++ template declarations
– Generic version of the bounded queue example
Can C++ be made as safe as SPARK?
29
Future work (1)
• Template instantiation preconditions
– Declare the auxiliary operators etc. needed to instantiate a template
template<class X> void sort(Array<X<> table)
require bool operator<(X, X)
post(…)
{
…
}
• Semantics of different sorts of volatile variables
– Currently, we treat all volatile variables as subject to unpredictable
changes in value, so they can’t be used in specifications
– But not all volatile variables can change unpredictably at all times
– SPARK 2014 uses the concept of external state to handle this
Can C++ be made as safe as SPARK?
30
Future work (2)
• Concurrency
–
–
–
–
C++ 2011 has a concurrency model
Shared-variable concurrency in general is a difficult problem
Microsoft’s Vcc handles it to some degree, but the annotation is hard
Taming Concurrency project (Cliff Jones, Newcastle)
• Floating point arithmetic
– If we model FP arithmetic as real arithmetic, we can do useful things, but
can also produce false proofs, e.g. 3.0 * (1.0/3.0) == 1.0
– If we model FP arithmetic more accurately, a lot of “useful” things
become unprovable
– In simple cases, range arithmetic may be suitable
Can C++ be made as safe as SPARK?
31
Related work
• Larch/C++ project
– Defined an annotation language, but not supported by verification tools
• Several formal verification systems for C
– Frama/Jessie, Vcc, Verifast, and our own eCv
Can C++ be made as safe as SPARK?
32
Conclusion
• By adding selected C++ features to MISRA-C:2012 we have defined
a subset of C++ that we believe is suitable for high-integrity software
– and offers substantial advantages over C
• Programs written in this subset can be verified formally in the same
way as programs written in the SPARK subset of Ada
• Applications of the tool so far:
– SIL 4 software in the defence industry
– Medical equipment (joint work with Newcastle University)
• Questions?
Can C++ be made as safe as SPARK?
33