Weakest (Liberal) Pre-Conditions and Proof-Carrying Code Consider trying to prove the partial correctness condition: {A1} c1;c2 {A2} One would hope that we can use the sequencing rule: {A1}c1{A} {A}c2{A2} -------------------- {A1} c1;c2 {A2} But how do we know that, when {A1} c1;c2 {A2} is true there exists an assertion A such that {A1}c1{A} and {A}c2{A2}? Perhaps our assertion language is too weak to capture the set of states that correspond to this intermediate step of the computation. That is, our assertion language might not be powerful enough to describe all of the states that we need. Fortunately it is. Unfortunately, we don't have enough time to prove that this is the case. But I'm going to give you a feel for how this can be done using something called Weakest (Liberal) Pre-Conditions. Let us define wp[c,A] (weakest liberal pre-condition of a command c and an assertion A) as follows: wp[c,A] = { s | C[c]s is undefined or C[c]s = s' and s' |= A } That is, wp[c,A] is the set of all states s such that, when we run the command c in state s, we either diverge or else we get an output state that satisfies the assertion A. Theorem: For each command c and assertion A2, there exists an assertion A1 such that wp[c,A2] = { s | s |= A1 }. In English, this just says that there is some assertion that can be used to precisely describe the weakest (liberal) pre-conditions of a given command and post-condition. (Note: We say weakest "liberal" pre-condition to distinguish from true weakest pre-conditions which don't allow for non- termination and are typically used when proving total correctness instead of partial correctness.) So, if we want to prove {A1} c1;c2 {A2}, it suffices to calculate wp[c2,A2] to generate an assertion A and then show that {A1}c{A} holds. For then we can use the sequencing rule, and the definition of wp[c2,A2] to conclude {A1} c1;c2 {A2}. Here are the weakest liberal pre-conditions for most of the commands: wp[skip, A] = A wp[x:=e, A] = A[e/x] wp[c1;c2, A] = wp[c1, wp[c2, A]] wp[if e then c1 else c2, A] = (e ^ wp[c1,A]) or (not(e) ^ wp[c2,A]) The wp for a while loop is pretty complicated (and not practical) so I won't describe it here. (See Winskel for details if you are interested.) It's pretty easy to convince yourself (via induction on commands) that: { wp[c,A] } c { A } The only interesting case above is for if-commands: {(e ^ wp[c1,A]) or (not(e) ^ wp[c2,A])} if e then c1 else c2 {A} Recall the Hoare rule for if: {e ^ A'}c1{A} {not(e) ^ A'}c2{A} ---------------------------------- {A'} if e then c1 else c2 {A} Substituting (e ^ wp[c1,A]) or (not(e) ^ wp[c2,A]) for A' above, we must show: {e ^ ((e ^ wp[c1,A]) or (not(e) ^ wp[c2,A]))} c1 {A} and {not(e) ^ ((e ^ wp[c1,A]) or (not(e) ^ wp[c2,A]))} c2 {A} For the first case, we can simplify: e ^ ((e ^ wp[c1,A]) or (not(e) ^ wp[c2,A])) == (e ^ e ^ wp[c1,A]) or (e ^ not(e) ^ wp[c2,A]) == (e ^ wp[c1,A]) or false == e ^ wp[c1,A] Similarly, the second case simplifies: not(e) ^ ((e ^ wp[c1,A]) or (not(e) ^ wp[c2,A])) == not(e) ^ wp[c2,A] So we only have to show that: { e ^ wp[c1,A] } c1 {A} and { not(e) ^ wp[c2,A] } c2 {A} By induction, we have that {wp[ci,A]} ci {A}, and certainly e ^ wp[c1,A] => wp[c1,A] and not(e) ^ wp[c2,A] => wp[c2,A] so we are done. What's not so easy to see is that if {A'}c{A}, then A' => wp[c,A]. That is, wp[c,A] really is the weakest possible assertion that we can use for the pre-condition on states to run c and get out states described by A. (Hint, hint, hint.) The nice thing about wp is that, if you give me a (while-loop free) program c, I can automatically reduce checking whether {A1}c{A2} holds to just checking whether A1 => wp[c,A2]. That's something that we could feed to a general purpose theorem prover for predicate logic (e.g., NuPRL, Boyer Moore, HOL, etc.) Even if you add while-loops back in, as long as the while loops are annotated with an explicit loop-invariant, then calculating weakest pre-conditions is easy (though not necessarily complete -- if you pick a bad invariant, then you might end up stuck in your proof. That is, it may not be that A1 => wp[c,A2] even though {A1}c{A2} is true.) -------------------------------------------------------------- An aside on Proof-Carrying Code (PCC): Suppose I send you an IMP program c, and I claim that it satisfies some specification {Pre}c{Post}. Suppose further that c includes loop invariants labelled on each while loop. Now suppose that I give you a machine-checkable proof that Pre => wp[c,Post]. By machine-checkable, I mean that we've formally specified the assertion language and the proof rules that you can use to construct a valid proof of an assertion in predicate logic. You can check the proof by running over the tree and validating that each node is an instance of one of the proof rules (and that the sub-proofs are valid and so on.) This is a simple walk over the tree that can be coded in a trustworthy manner (about 2-3 pages of ML code.) Now suppose that your proof checker says "Yep, this is a valid proof that Pre => wp[c,Post]". What can we conclude? Well, we know that if you run c in any input state satisfying Pre, then if c terminates, it will end up in a state satisfying Post. So, in summary, if I send you a command c annotated with loop invariants, and I send you a proof of Pre => wp[c,Post], then you can automatically check that c does what it claims to do. In a modern setting, this corresponds to downloading untrusting code from some site and being able to verify that the program does what it claims to do. What happens if we tamper with c? For instance, suppose we change it to c'? Well, then when we run wp[c',Post], we get a (possibly) different assertion than if we run wp[c,Post]. So, our proof that Pre => wp[c,Post] won't allow us to conclude Pre => wp[c',Post], so we'll reject the code. Actually, in some cases, it could be that wp[c',Post] = wp[c',Pre] (e.g., if I just insert some bogus skip statements or x := x statements.) But note that these won't change the behavior of the program! Now suppose I tamper with the proof P that Pre => wp[c,Post]. Well, you're checking the proof, so if the tampering results in an invalid proof, you'll catch it. But maybe the tampering generates a valid proof, but of a different assertion (e.g., Pre' => wp[c',Post']). Well, then you'll check that the assertion doesn't match what you're looking for. In short, if I send you code and a proof, then we can use wp to tie the two together in a tamper-proof way. This is the way that proof-carrying code works. If you want to read more about PCC, then see the following: http://raw.cs.berkeley.edu/pcc.html PCC has been used in real systems to ensure that, for instance, a downloaded module into a kernel does not violate the security policy of the kernel. It has also been used in other settings (think smartcards, phones, etc.) where security and/or reliability are crucial. PCC eliminates the need to trust a piece of code. However, there are two really hard problems that PCC does not address: (1) how do we formalize the specification of what a program should/ shouldn't be able to do for a specific environment (e.g., a kernel)? (2) how do we construct proofs that Pre => wp[c,Post]? Solving these two problems is an active area of research these days.