Verifying Stateful Programs with F*

fstar-logo

Cătălin Hriţcu, Inria Paris

(with help from Kenji Maillard)

Models and Tools for Cryptographic Proofs
Nancy, France, 10-13 July 2017

Verifying stateful programs

Variant #1: intrinsically (at definition time)

  • State in F*: mutable references

    val incr : r:ref int -> St unit
    let incr r = (r := (!r + 1))
  • Hoare logic-style pre- and post-conditions

    val incr : r:ref int -> ST unit (requires (fun h0 -> True))
                               (ensures (fun h0 _ h1 -> sel h1 r = sel h0 r + 1))
  • Beyond what can be expressed with refinements

  • Now pre-condition is relation on initial states; post-condition is relation on initial states, result values, and final states

    • the more complex the effect, the more complex the specs (and proofs)

Heap and ST operations (simplified)

module FStar.Heap

  val heap : Type
  val ref : Type -> Type

  val sel : #a:Type -> heap -> ref a -> Tot a
  val upd : #a:Type -> heap -> ref a -> a -> Tot heap

  val selUpdEq : #a:Type -> h:heap -> r:ref a -> v:a ->
        Lemma (sel (upd h r v) r  == v) [SMTPat (sel (upd h r v) r)]
                   
  val selUpdNeq : #a:Type -> h:heap -> r1:ref a -> r2:ref a -> v:a ->
        Lemma (r1 =!= r2 ==>
               sel (upd h r1 v) r2 == sel h r2) [SMTPat (sel (upd h r1 v) r2)]
module FStar.ST
  open FStar.Heap

  val (!): #a:Type -> r:ref a -> ST a
    (requires (fun (h:heap) -> True))
    (ensures (fun (h0:heap) (x:a) (h1:heap) -> h0 == h1 /\ x == sel h0 r))

  val (:=): #a:Type -> r:ref a -> v:a -> ST unit
    (requires (fun (h:heap) -> True))
    (ensures (fun (h0:heap) (x:a) (h1:heap) -> h1 == upd h0 r v))

Verifying incr (intuition)

module FStar.ST

  val (!) : #a:Type -> r:ref a -> ST a
    (requires (fun h0 -> True))
    (ensures (fun h0 x h1 -> h0 == h1 /\ x == sel h0 r))

  val (:=) : #a:Type -> r:ref a -> v:a -> ST unit
    (requires (fun h -> True))
    (ensures (fun h0 x h1 -> h1 == upd h0 r v))
val incr r:ref int -> ST unit (requires (fun h0 -> True))
            (ensures (fun h0 _ h2 -> exists h1 x. h0 == h1 /\ x == sel h0 r /\
                                                  h2 == upd h1 r (x + 1)))
let incr r = let x = !r in r := x + 1
val incr : r:ref int -> ST unit (requires (fun h0 -> True))
                           (ensures (fun h0 _ h2 -> sel h2 r = sel h0 r + 1))
sel (upd h0 r ((sel h0 r) + 1)) r = sel h0 r + 1
sel h0 r + 1 = sel h0 r + 1

Swapping two references

val swap_add_sub : r1:ref int -> r2:ref int -> St unit
let swap_add_sub r1 r2 =
  r1 := !r1 + !r2;
  r2 := !r1 - !r2;
  r1 := !r1 - !r2

let main =
  let r1 = alloc 1 in
  let r2 = alloc 2 in
  swap_add_sub r1 r2;
  print_string ("r1=" ^ string_of_int !r1 ^ "; " ^
                "r2=" ^ string_of_int !r2 ^ "\n")
...
r1=2; r2=1

Specifying reference swapping

val swap : r1:ref int -> r2:ref int -> ST unit
    (requires (fun h' -> True ))
    (ensures (fun h' _ h ->
                sel h r1 = sel h' r2 /\ sel h r2 = sel h' r1))
let swap r1 r2 =
  let t = !r1 in
  r1 := !r2;
  r2 := t

Hand proof sketch in code/03/SwapVerify.fst

Exercise: specify imperative factorial

val factorial_tot : nat -> Tot nat
let rec factorial_tot x = if x = 0 then 1 else x * factorial_tot (x - 1)

(* TODO: write a stronger ensures clause for factorial that proves
         it does the same thing as factorial_tot *)
val factorial : r1:ref nat -> r2:ref nat -> ST unit
      (requires (fun h' -> True))
      (ensures (fun h' a h -> True))
let rec factorial r1 r2 =
  let x1 = !r1 in
  if x1 = 0
  then r2 := 1
  else 
    (r1 := x1 - 1;
     factorial r1 r2;
     r2 := !r2 * x1)

Stateful invariants

  let r = alloc #nat 0 in
  r := 1;
  r := 2
(*   r := -2 -- expected type Prims.nat; got type Prims.int *)

Monotonic references, by example

let increases (n m:nat) : Type0 = n <= m

let incr (r:mref nat increases) : ST unit (requires (fun _ -> True))
                                          (ensures (fun _ _ h -> sel h r > 0))
  = r := !r + 1

(* let decr (r:mref nat increases) : St unit =  -- fails as it should
  r := (!r - 1) *)
assume val complex_procedure (r:mref nat increases) : St unit
let nonzero (n:nat) : Type0 = n > 0

let main() : St unit =
  let r = alloc increases 0 in
  incr r;
  witness r nonzero;
  complex_procedure r;
  let x = !r in
  (* assert (x > 0); -- fails as it should *)
  recall r nonzero;
  assert (x > 0)

Monotonic references interface (part 1)

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

type preorder (a:Type) = rel:relation a{(forall (x:a). rel x x)
    /\ (forall (x:a) (y:a) (z:a). (rel x y /\ rel y z) ==> rel x z)}

Examples: $\leq$, $\subseteq$, $\to^*$,

val alloc : #a:Type -> preorder a -> a -> ST (mref a rel) ...
val (:=) : #a:Type -> #rel:preorder a -> r:mref a rel -> v:a -> : ST unit
  (requires (fun h -> rel (sel h r) v))
  (ensures (fun h0 x h1 -> h1 == upd h0 r v))

Stable predicates, witness and recall (part 2)

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

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

witness: #a:Type -> #rel:preorder a -> m:mref a rel -> p:(pred a){stable p rel}
                        -> ST unit (requires (fun h0 -> p (sel h0 m)))
                                   (ensures (fun h0 _ h1 -> h0==h1 /\ token m p))

recall: #a:Type -> #rel:preorder a -> m:mref a rel -> p:(pred a){stable p rel}
                    -> ST unit (requires (fun _ ->  token m p))
                               (ensures (fun h0 _ h1 -> h0==h1 /\ p (sel h1 m)))

Simple monotonic log

let subset' (#a:eqtype) (l1:list a) (l2:list a)
  = (forall x. x `mem` l1 ==> x `mem` l2)
let subset (#a:eqtype) : Tot (preorder (list a)) = subset'

let lref = mref (list int) subset

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

Preorder for Initializing and Freezing references

type rstate (a:Type) =
  | Empty : rstate a
  | Mutable : v:a -> rstate a
  | Frozen : v:a -> rstate a
let evolve (a:Type) = fun r1 r2 ->
  match r1, r2 with
  | Empty, Mutable _
  | Mutable _, Mutable _ -> True
  | Mutable v1, Frozen v2 -> v1 == v2
  | _, _ -> False
(* let evolve (a:Type) : Tot (preorder (rstate a)) = evolve a *)

This fails to typecheck! Why is this? Can you fix it?

let evolve' (a:Type) = fun r1 r2 ->
  match r1, r2 with
  | Empty, _
  | Mutable _, Mutable _
  | Mutable _, Frozen _  -> True 
  | Frozen v1, Frozen v2 -> v1 == v2
  | _, _ -> False
let evolve (a:Type) : Tot (preorder (rstate a)) = evolve' a

< TODO: had a hand drawing on this on the board *)

Initializing and Freezing references

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)
assume val complex_procedure (r:eref int) : St unit
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)

Giving specs to our new primitives

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

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

let read (#a:Type) (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:Type) (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:Type) (r:eref a) : ST ...
  = r := Frozen (Mutable?.v !r)

Exercise: write a type for freeze so that things typecheck

Exercise: write a function that writes an arbitrary non-frozen reference and freezes it at the same time.

Verifying stateful programs

Variant #2: extrinsically (by monadic reification)

  • Revealing pure effect representation

      STATE.reify (e : ST a pre post)
        : n0:nat -> Pure (a * nat) (requires (pre n0))
                                   (ensures (fun r -> post n0 (fst r) (snd r))
  • Allows us to give weak specification to an effectful function

      let incr () : ST a (requires (fun _ -> True)) (ensures (fun _ _ _ -> True))
        = let n = STATE.get() in STATE.put (n + 1)
  • then prove lemmas about reification of effectful computation

      let incr_increases (s0:s) : Lemma (snd (ST.reify (incr()) s0) = s0 + 1) = ()
  • reify only when the abstraction permits (e.g. ghost code)

Some dreams and ongoing work on F*

  • Find best balance between automation and control

    • adding tactics à la Lean/Idris, using F* itself for meta-programming
  • Fully work out metatheory of F*

    • formalizing monotonic state and F* to SMT encoding
    • longer term: semantic model; self-certify implementation
  • Verify and deploy new, efficient HTTPS stack (Everest)

    • verified interoperability of F* (OCaml), Low* (C), and Vale (ASM)
  • Opportunities brought in by extensible effect system

    • beyond ML effects: probabilities, concurrency,
    • relational verification, e.g. for more flexible confidentiality proofs

F*: verification of effectful programs

  • Functional programming language with effects

  • Semi-automated verification system using SMT

  • Interactive proof assistant based on dependent types

  • Open source, code on GitHub

  • Tutorial, papers, slides at fstar-lang.org

  • PhD internships at Microsoft Research (Cambridge, Redmond, Bangalore); application in Dec 2017 - Jan 2018

  • Next: Code-Based Cryptographic Verification in F* (Markulf)