330 likes | 345 Views
This article compares the software integrity levels of SPARK with C/C++, discussing formal verification, implementation examples, and encapsulation in both languages.
E N D
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 required 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 isprivate; function empty(q: in Queue) return Boolean; function full(q: in Queue) return Boolean; procedure add(q: inout Queue; val: in Character) with Pre => not full(q) ; procedure remove(q: inout Queue; val: out Character) with Pre => not empty(q) ; procedure init(q: out Queue) with Post => empty(q); privatesubtype StorageIndex is Integer range 0..capacity; type Storage isarray(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) typedefstruct{char data[CAPACITY + 1u];unsigned int nextIn; // ranges from 0..CAPACITYunsigned int nextOut; // ranges from 0..CAPACITY} Queue; 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 • C has no adequate mechanism for data hiding • Can we do better with C++? type Queue isprivate; … private…type Queue is record data: Storage; nextIn: StorageIndex; nextOut: StorageIndex;end record; typedefstruct{char data[CAPACITY + 1u];unsigned int nextIn; unsigned int nextOut; } Queue;
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; // ranges from 0..CAPACITYunsigned int nextOut; // 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 uses 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: • How to do this in C++? • Maybe do it the way SPARK used to, in comments? procedure add(q: inout Queue; val: in Character) with Pre => not full(q) ; procedure remove(q: inout Queue; val: out Character) with Pre => not empty(q) ; procedure init(q: out Queue) with Post => empty(q); 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; // ranges from 0..CAPACITYunsigned int nextOut; // 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: • 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 #define pre(expression) // nothing … void add(char c)pre(!full()); 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; // ranges from 0..CAPACITYunsigned int nextOut; // 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: • 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++? subtype StorageIndex is Integer range 0..capacity; type Storage isarray(StorageIndex) of Character; type Queue is record data: Storage; nextIn: StorageIndex; nextOut: StorageIndex;end record; Can C++ be made as safe as SPARK? 13
Adding range-constrained types to C++ • We could use a class template: • If we don’t need run-time checking, we can use annotations instead 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; Can C++ be made as safe as SPARK? 14
Extending the C++ typedef declaration • C and C++ allow you to define synonyms for types: • Let’s add constraints to typedef declarations: • 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” typedef size_t StorageIndex; #define invariant(expression) // nothing … typedef size_t invariant(value <= capacity) StorageIndex; 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:typedefunsigned intinvariant(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 isfunction empty(q: in Queue) return Boolean isbeginreturn q.nextIn = q.nextOutend; function full(q: in Queue) return Boolean isbeginreturn (q.nextIn + 1) mod (capacity + 1) = q.nextOut;end; procedure add(q: inout Queue; val: in Character) isbegin q.data(q.nextIn) := val; q.nextIn := (q.nextIn + 1) mod (capacity + 1);end; procedure remove(q: inout Queue; val: out Character) isbegin 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)); -- for SPARKend; end BoundedQueue;
// 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 • 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 Remove elements from head Add elements to tail 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 functionghost( ecv_seq<char> contents() constreturns( (nextIn >= nextOut) ? data.take(nextIn).drop(nextOut) : data.drop(nextOut).concat(data.take(nextIn)) ); ) bool empty() constreturns(contents().count == 0); bool full() constreturns(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 • 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 template<class X> void sort(Array<X> table)requirebooloperator<(X, X)post(…){ … } 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