Monotonic State in F*

fstar-logo

Danel Ahman, Inria Paris

EPIT 2018 Spring School

Aussois, France, 7-11 May 2018

Schedule

  • Cătălin: A Gentle Introduction to F*:
    Verifying purely functional programs

  • Cătălin: Verifying Stateful Programs in F*

  • Danel: Monotonic State in F*

  • Cătălin: Monadic effects in F* and Dijkstra Monads; F*'s SMT encoding

  • Antoine: Security verification and cryptographic modelling in F*

Please ask questions any time!

State in F*

(* In the last lecture, you saw how to use F* to verify simple stateful programs, e.g., *)

val sum_st : n:nat -> ST nat (requires (fun h0      -> True))
                             (ensures  (fun h0 x h1 -> x == sum_rec n /\
                                                       modifies !{} h0 h1))

(* In this lecture, you will see that standard refs alone are often unsatisfactory *)


(* WARNING: I will omit trivial requires preconditions on my slides, e.g.,  *)

val sum_st : n:nat -> ST nat (ensures (fun h0 x h1 -> x == sum_rec n /\
                                                      modifies !{} h0 h1))

Hello World (aka Monotonic Counters)

(* A simple interface to monotonic counters *)

val create: i:int     -> ST counter (ensures (fun h c h' -> fresh c h h' /\ ...))
val read  : c:counter -> ST int     (ensures (fun h i h' -> sel h' c = i /\ h == h'))
val incr  : c:counter -> ST unit    (ensures (fun h _ h' -> sel h' c = sel h c + 1 /\ ...))
(* An example program using monotonic counters *)

let main() : St unit = 
  let c = create 0 in incr c;

  let i = read c in assert (i > 0);

  complex_procedure c;                   (* complex_procedure : counter -> St unit *)
  
  let j = read c in assert (j > 0)       (* (Error) assertion failed *)
(* A naive but unsatisfactory fix for this particular example *)

complex_procedure : c:counter -> ST unit (requires (fun h      -> sel h c > 0))
                                         (ensures  (fun _ _ h' -> sel h' c > 0))

Uses of Monotonic State in F*

  1. For implementing a range of memory models

    • Untyped references
    • Typed references
    • Monotonic references
    • Region-based hyper-heaps and hyper-stacks
  2. Reasoning about various kinds of counters

  3. Reasoning about idealized monotonic logs

  4. Initializing and freezing references and arrays

Monotonic State in F* (part 1)

(* The main user-facing interface is monotonic references (see FStar.Monotonic.Heap) *)

type mref (a:Type0) (rel:preorder a) = ... (* if time, in the end of the lecture *)

(* where *)

type relation (a:Type) = a -> a -> Type0

type preorder (a:Type) = rel:relation a{
  (forall (x:a). rel x x) /\ (forall (x y z:a). rel x y /\ rel y z ==> rel x z)}
(* Accompanied by stateful actions (see FStar.ST) *)

val alloc : #a:Type0 -> #rel:preorder a -> init:a ->
  ST (mref a rel) (ensures (fun h0 r h1 -> fresh r h0 h1 /\ sel h1 r == init /\ ...))

val (!) : #a:Type0 -> #rel:preorder a -> r:mref a rel ->
  ST a (ensures (fun h0 x h1 -> h0 == h1 /\ sel h1 r == x))

val (:=) : #a:Type0 -> #rel:preorder a -> r:mref a rel -> v:a ->
  ST unit (requires (fun h0      -> rel (sel h0 r) v))         (* note the precondition *)
          (ensures  (fun h0 _ h1 -> modifies !{r} h0 h1 /\ sel h1 r == v))

Monotonic State in F* (part 2)

(* We can now implement monotonic counters as follows *)

let counter = mref int (fun n m -> n <= m)

let create i = alloc i                                        (* fresh c h h' *)
let read   c = !c                                             (* sel h' c = i *)
let incr   c = c := (!c + 1)                                  (* sel h' c = sel h c + 1 *)
(* But by themselves these definitions are not enough to verify the example *)

let main() : St unit = 
  let c = create 0 in incr c;

  let i = read c in assert (i > 0);
  complex_procedure c;                   (* complex_procedure : counter -> St unit *)
  let j = read c in assert (j > 0)       (* (Error) assertion failed *)
(* Solution idea: Observe that (fun j -> j > 0) is a stable predicate wrt <= *)

let stable (#a:Type) (p:predicate a) (rel:preorder a) 
  = forall (x y:a). p x /\ rel x y ==> p y

Monotonic State in F* (part 3)

(* To make easy use of this idea in verification, F* defines (in FStar.MRef): *)
(* A new pure proposition witnessing the validity of a stable predicate *)

type token (#a:Type0) (#rel:preorder a) (r:mref a rel) (p:a -> Type0)
(* Two stateful actions to witness and recall such tokens *)

val witness_token : #a:Type0 -> #rel:preorder a -> r:mref a rel -> p:(a -> Type0) -> 
  ST unit (requires (fun h0      -> p (sel h0 r) /\ stable p rel))
          (ensures  (fun h0 _ h1 -> h0 == h1 /\ token r p))

val recall_token : #a:Type0 -> #rel:preorder a -> r:mref a b -> p:(a -> Type0) -> 
  ST unit (requires (fun _       -> token r p))
          (ensures  (fun h0 _ h1 -> h0 == h1 /\ p (sel h1 r)))
(* As a bonus, we also have recall "for free" for existence of GCd references *)

val recall : #a:Type0 -> #rel:preorder a -> r:mref a rel ->
  ST unit (ensures (fun h0 _ h1 -> h0 == h1 /\ h1 `contains` r))

Hello World (revisited)

(* Recall that we defined monotonic counters as follows *)

let counter = mref int (fun n m -> n <= m)

let create i = alloc i
let read   c = !c
let incr   c = c := (!c + 1)
(* Using witness_token and recall_token, we can finally verify the example *)

let main() : St unit = 
  let c = create 0 in incr c;

  let i = read c in assert (i > 0);

  witness_token c (fun i -> i > 0);
  complex_procedure c;                   (* complex_procedure : counter -> St unit *)
  recall_token c (fun i -> i > 0);
  
  let j = read c in assert (j > 0)       (* success *)

Typed References

(* Typed references from Catalin's lectures are just an instance of mrefs *)

type ref a = mref a (fun _ _ -> True)

 

(* While we can't use tokens any more, we still get containment "for free" for GCd refs *)

let recall (#a:Type0) (r:ref a)
  : ST unit (ensures  (fun h0 _ h1 -> h0 == h1 /\ h1 `contains` r))

  = MonotonicStateSlide.recall r

Simple Monotonic Log

(* We model monotonic logs using monotonic references to lists of ints  *)

let lref = mref (list int) subset

(* where *)

let subset (l1 l2:list int) = forall x . x `mem` l1 ==> x `mem` l2
(* Derived syntax for adding new elements to the log *)

let add_to_log (r:lref) (v:int) : ST unit (ensures  (fun _ _ h -> v `mem` (sel h r)))
  = r := (v :: !r)
let main() : St unit =
  let r = alloc subset [] in add_to_log r 42;
  
  witness r (fun xs -> 42 `mem` xs);
  complex_procedure r;                          (* complex_procedure : lref -> St unit *)
  recall r (fun xs -> 42 `mem` xs);

  let xs = !r in assert (42 `mem` xs)

Initializing and Freezing references (part 1)

(* Contents of our initializable and freezable references (a value + a ghost state) *)

type rstate (a:Type0) =
  | Empty   : rstate a
  | Mutable : v:a -> rstate a
  | Frozen  : v:a -> rstate a
(* Defining a preorder for initialization and freezing *)

let evolve' (a:Type0) = fun (r1 r2:rstate a) -> match r1 , r2 with
  | Empty      , Mutable _
  | Mutable _  , Mutable _ -> True
  | Mutable v1 , Frozen v2 -> v1 == v2
  | _          , _         -> False
  
let evolve (a:Type0) : preorder (rstate a) = evolve' a  (* (Error) *) (* Exercise: Why? *)
  | Empty     , _
  | Mutable _ , Mutable _
  | Mutable _ , Frozen  _ -> True 
  | Frozen v1 , Frozen v2 -> v1 == v2
  | _         , _         -> False

Initializing and Freezing references (part 2)

let eref (a:Type0) : Type = mref (rstate a) (evolve a)

let alloc a : ST (eref a) (ensures  (fun _ r h -> Empty? (sel h r)))
  = alloc (evolve a) Empty

let read #a (r:eref a) : ST a (requires (fun h      -> ~(Empty? (sel h r))))
                              (ensures  (fun h v h' -> h == h' /\
                                                       (sel h r == Mutable v \/
                                                        sel h r == Frozen v     )))
  = match (!r) with | Mutable v | Frozen v -> v

let write #a (r:eref a) (v:a) : ST unit (requires (fun h     -> ~(Frozen? (sel h r))))
                                        (ensures  (fun _ _ h -> sel h r == Mutable v))
  = r := Mutable v

let freeze #a (r:eref a) : (* Exercise: What is the type of freeze? *)
  = r := Frozen (Mutable?.v !r)
ST unit (requires (fun h0      -> Mutable? (sel h0 r)))
        (ensures  (fun h0 _ h1 -> Frozen? (sel h1 r) /\
                                  Mutable?.v (sel h0 r) == Frozen?.v (sel h1 r)))

Initializing and Freezing references (part 3)

let alloc  (a:Type)                   = alloc (evolve a) Empty
let read   (#a:Type) (r:eref a)       = match (!r) with | Mutable v | Frozen v -> v
let write  (#a:Type) (r:eref a) (v:a) = r := Mutable v
let freeze (#a:Type) (r:eref a)       = r := Frozen (Mutable?.v !r)
let main() : St unit =
  let r = alloc int in
  (* ignore (read r) -- fails like it should *)
  write r 42;
  ignore (read r);
  write r 0;
  witness r (fun rs -> ~(Empty? rs));
  freeze r;
  (* write r 7; -- fails like it should *)
  ignore (read r);
  witness r (fun rs -> rs == Frozen 0);
  complex_procedure r;
  (* ignore (read r); -- fails like it should *)
  recall r (fun rs -> ~(Empty? rs));
  let x = read r in
  (* assert (x == 0) -- fails like it should *)
  recall r (fun rs -> rs == Frozen 0);
  assert (x == 0)

Homework: Initializable and Freezable Arrays

  1. Arrays are created uninitialized

  2. Only initialized elements can be read from arrays

  3. Arrays can be mutated until they are frozen

  4. Only fully initialized arrays can be frozen

  5. Frozen arrays can only be read

    • no mutation of array elements

    • no initialization preconditions for reading

Questions?



For more information:


D. Ahman, C. Fournet, C. Hritcu, K. Maillard, A. Rastogi, N. Swamy
Recalling a Witness: Foundations and Applications of Monotonic State
POPL 2018

Bonus Material: How the Sausage is Made


(see FStar.Monotonic.Heap, FStar.ST, and FStar.MRef)

Bonus Material: Temporarily Escaping the Preorder


(see Snapshots.fst)