-
Notifications
You must be signed in to change notification settings - Fork 232
Steel
The Steel framework is being implemented on the denismerigoux_steel
branch.
Verifying Low* programs has turned out to be a quite popular activity across all major F* projects like Evercrypt, miTLS, Everparse, etc. The current memory model augmented with dynamic frames and the theory of modifies clauses allows you to get the work done. However, there are a number of pain points that really limit the scalability of Low*:
- Because Z3 is pushed to its limits with high
--z3rlimit
, proofs are flaky and fixing that requires knowing wich lemma to call among the gigantic lemma supermarket ofLowStar.Monotonic.Buffer
. - If you don't want to use specific lemmas, the only solution is to split up functions into small bits and specify each bit separately. However the maximum number of stateful operations that you can put in one function, which seems to be around 8~10, is extremely low.
- Specifying complex memory invariants, for instance for data structures, is extremely difficult (e.g. doubly-linked lists).
For all of these reasons, we decided to have a more principled way of specifying the shape of the heap and how it changes. That is where Steels arrives.
A function specification in Low* usually looks like that:
val swap (x: pointer int) (y: pointer int) : ST unit
(requires (fun h -> live x h /\ live y h /\ disjoint x y))
(ensures (fun h0 _ h1 ->
modifies (x `loc_union` y) /\
get h1 x = get h0 y /\ get h1 y = get h0 x
))
You can see that this specification has two parts : one describing the shape of the heap with live
and modifies
, and another describing what happens to the contents of the two pointers. You only need the heap shape part to specify memory safety, the rest concerns functional correctness. The initial goal of Steel was to separate those two parts of the specification, and handle the memory safety part in a automated and reliable way without clogging the SMT.
For that, we introduced a new way to specify how the heap is organized and how it changes. This new way is based on a new type, resource
. resource
s are only meant to be a specification tool; they are never extracted. However they encapsulate everything you need to know about an object living on the heap:
- a footprint, which is the
loc
it corresponds to; - an invariant, of type
HS.mem -> prop
, which should contain things likelive
that are true for all the lifespan of the object; - a view, which is the high-level
Tot
equivalent of your object (e.g.Seq.seq
for arrays).
Resources are specifications that are built upon regular F* values that are passed around inside your programs. For instance if your function has a pointer argument p
, then you can talk about the associated resource using ptr_resource p
.
Let's try to use resources to describe the state of the heap at the beginning of our swap
function. We have two pointers, x
and y
. The corresponding resources are ptr_resource x
and ptr_resource y
. However we want to say that x
and y
are disjoint. Steel has a nice operator for that: <*>
. It is the separation logic star, with the same meaning. So here's the description of our heap before the swap
function: ptr_resource x <*> ptr_resource y
. This description is also valid for the heap after swap
, since only the contents of the pointers changed.
Now that we can describe heaps using resources, let's specify functions using that mechanism. For that, just use the RST
effect which has 5 indexes:
RST (a: Type) // Return type
(pre_resource: resource) // Heap before, decribed with a resource
(post_resource: a -> resource) // Heap after, described with a resource
(pre: rmem pre_resource -> prop) // Other preconditions
(post: rmem pre_resource -> a -> rmem post_resource -> prop) // Other postconditions
Let's immediately take an example with our swap
function:
val swap (x: pointer int) (y: pointer int) : RST unit
(ptr_resource x <*> ptr_resource y)
(fun _ -> ptr_resource x <*> ptr_resource y)
(fun h0 -> True)
(fun h0 _ h1 ->
get x h1 = get y h0 /\ get y h1 = get x h0
)
Why five indexes ? The second and third indexes allows you to specify your function using separation logic, while the fourth and fifth have the same meaning as the old requires
and ensures
indexes of the ST
effect. While you should be at least use separation logic to describe the shape of the heap, you can also encapsulate more information in the resource
specification. The hsrefine
combinator allows you to define, for instance, a pointer resource pointing to a specific value:
let points_to (#a: Type) (p: pointer a) (v: a) : resource = hsrefine (ptr_resource p) (fun h -> get p h == v)
This system allows you to choose between two styles of specification for your functions, ranging from completely functional to completely stateful.
When implementing functions, each call to a RST
function should make clears what is the part of the resource context that is touched, and what part is untouched. For that, we use an equivalent of the frame rule of separation logic. For instance, if your resource context at some point in the function is ptr_resource x <*> ptr_resource y
and that you want to allocate a new pointer, you should write:
...
let z = rst_frame
(ptr_resource x <*> ptr_resource y)
(fun z -> ptr_resource x <*> ptr_resource y <*> ptr_resource z)
(fun _ -> ptr_alloc 0ul)
in
...
By encapsulating all of the RST
calls inside rst_frame
, Steel is aware of the resource context at each point of the program and can effectively prove memory safety using separation logic.
In Steel, the view of your resources also contains a ghost FStar.Real
number representing the fractional permission that your resource owns over the heap object. These permissions are set up such that they are strictly positive if the heap object is live, and you need full permission (=1
) to modify the contents of the objects on the heap. The libraries provide functions allowing you to manipulate your object's permissions. For instance, here is a simplified example from the array resource library (LowStar.RST.Array
):
val share (#a:Type) (b:A.array a) : RST (A.array a)
(array_resource b)
(fun b' -> array_resource b <*> array_resource b')
(fun _ -> True)
(fun h0 b' h1 ->
get_rperm b h1 == P.half_permission (get_rperm b h0) /\
get_rperm b' h1 == P.half_permission (get_rperm b h0) /\
)
share
creates a new, disjoint resource that is a read-only copy of the original resource. Since it halves the permissions of both objects, you will not be able to modify the contents of the arrays. That is why we can consider them disjoint even though they alias the same heap object.
TODO: @afromherz
As of August 2019, Steel is only usable by expert users. For instance, you have to write the rst_frame
call explicitely each time. Here is a todo-list for reaching a beta stage:
- Steel: surface-language
- More fine-tuning of SMT queries
- Additional libraries
- Complete interoperation with Low*
- Concurrency: deadlock prevention
- Concurrency: complete proof of soundness
Here is the architecture of the libraries:
----------------------- -------------
| LowStar.Permissions | | FStar.MG2 |
----------------------- -------------
↓ ↓ ↓
----------------- --------------------
| LowStar.Array | | LowStar.Resource |
----------------- --------------------
| ↓
| ---------------
| | LowStar.RST |
| ---------------
↓ ↓ ↓
--------------------- ---------------------
| LowStar.RST.Array | | LowStar.RST.Loops |
--------------------- ---------------------
↓ ↓
----------------------- --------------------------
| LowStar.RST.Pointer | → | LowStar.RST.LinkedList |
----------------------- --------------------------
Here is the typical header for a Steel module:
open LowStar.RST
module A = LowStar.RST.Array
module P = LowStar.Permissions