-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Best practice for proving loop termination in firmware? #186
Comments
Our functions always terminate regardless of mmio input, the postconditions have a separare timeout case. If you can rely on the peripheral device completing its job within a fixed bound of mmio interactions, your way is better (or at least so I have believed without trying it out too hard). |
Where is the separate timeout case? Looking at the bedrock2/bedrock2/src/bedrock2/WeakestPrecondition.v Lines 89 to 97 in 9298bb5
Or do you mean something other than |
Ah, I think I now understand -- by "our programs" you mean the lightbulb functions, not bedrock2 programs in general. Okay, that makes sense to me. I think for my use-case I prefer to experiment with relying on the hardware guarantees to prove termination, but good to know why I differ from the examples here. Thanks! |
Just a follow-up about this in case someone is curious later on. Using the hardware spec to prove loop termination works just fine for proving the bedrock2 programs correct, but I think it's unworkable with the compiler proofs. I'm loosely following the example from bedrock2/compiler/src/compilerExamples/MMIO.v Lines 291 to 294 in 3084575
I run into problems because at some point I have a proof state that says essentially: l : locals
t : trace
m : mem
H1 : forall x : word, read_valid t x -> outcome map.empty [x]
H2 : forall mRecv val,
outcome mRecv [val] ->
postH (map.empty, READ, [addr], (mRecv, [val]) :: t) m (map.put l name val)
w : word
===================
exists finalTrace finalMem finalLocals,
postH finalTrace finalMem finalLocals
/\ map.extends (map.put initialLocals name w) finalLocals
/\ <other things to prove> Essentially, I need to use the conclusion of In summary: the compiler proofs require that |
I'm surprised by this issue, I fully expect that the compiler should be able to pass through extspec assumptions from hardware to software. |
It's definitely possible that I'm missing something or doing an unsafe rewrite somewhere. To provide a little more detail, then: I'm trying to prove a bedrock2/compiler/src/compiler/FlatToRiscvCommon.v Lines 332 to 356 in 3084575
I'll try to produce a minimal example. |
Minimal example pushed to https://github.com/jadephilipoom/bedrock2/tree/hardware-guarantees Diff from master (3084575): diff --git a/bedrock2/src/bedrock2/FE310CSemantics.v b/bedrock2/src/bedrock2/FE310CSemantics.v
index 46935987..3513ca66 100644
--- a/bedrock2/src/bedrock2/FE310CSemantics.v
+++ b/bedrock2/src/bedrock2/FE310CSemantics.v
@@ -39,6 +39,9 @@ Section WithParameters.
Definition isMMIOAligned (n : nat) (addr : parameters.word) :=
n = 4%nat /\ word.unsigned addr mod 4 = 0.
+ (* hardware spec : MMIO reads are always 0 *)
+ Definition read_valid (val : parameters.word) : Prop := val = word.of_Z 0.
+
Definition ext_spec (t : bedrock2_trace) (mGive : parameters.mem) a (args: list parameters.word) (post:parameters.mem -> list parameters.word -> Prop) :=
if String.eqb "MMIOWRITE" a then
exists addr val,
@@ -49,7 +52,7 @@ Section WithParameters.
exists addr,
args = [addr] /\
(mGive = Interface.map.empty /\ isMMIOAddr addr /\ word.unsigned addr mod 4 = 0) /\
- forall val, post Interface.map.empty [val]
+ forall val, read_valid val -> post Interface.map.empty [val]
else False.
Global Instance semantics_parameters : Semantics.parameters :=
@@ -75,7 +78,7 @@ Section WithParameters.
| H: exists _, _ |- _ => destruct H
| H: _ /\ _ |- _ => destruct H
| H: False |- _ => destruct H
- end; subst; eauto 8 using Properties.map.same_domain_refl.
+ end; subst; eauto 10 using Properties.map.same_domain_refl.
Qed.
Global Instance ok : Semantics.parameters_ok semantics_parameters.
diff --git a/compiler/src/compilerExamples/MMIO.v b/compiler/src/compilerExamples/MMIO.v
index e929eab4..c0c78026 100644
--- a/compiler/src/compilerExamples/MMIO.v
+++ b/compiler/src/compilerExamples/MMIO.v
@@ -470,6 +470,7 @@ Section MMIO1.
end.
cbv [ext_spec FlatToRiscvCommon.Semantics_params FlatToRiscvCommon.ext_spec FE310CSemantics.ext_spec] in Ex.
simpl in *|-.
+ assert (FE310CSemantics.read_valid (word.of_Z 0)) by reflexivity.
rewrite E in *.
destruct ("MMIOREAD" =? action)%string eqn:EE in Ex; try contradiction. The assertion in MMIO.v helps some earlier conditions go through by proving there exists some possible valid value; this is something I can prove with my real example as well if needed. |
I think you want to add a similar assumption to https://github.com/mit-plv/riscv-coq/blob/master/src/riscv/Platform/MinimalMMIO.v#L63 |
I'd expect that you need to change the semantics of the RiscvMachine to state that |
... and as I post this, I see that Andres thinks so too, but 2min before me 😅 |
Thanks both! I'm testing it out now and will report back 🙂 |
It works! 🎉 Thanks for the help! If I can make it compatible with existing examples, would you all be willing to consider a PR to |
Concretely, here's the diff: https://gist.github.com/jadephilipoom/1e8e432d27a6e41162b0ae023ffa9994 (have not tested examples yet) |
I think such a PR would make sense (once you've connected everything to confirm that it works). (* there exists at least one valid MMIO read value (set is nonempty) *)
/\ (exists v : HList.tuple byte n, MMIOReadOK n (getLog mach) a (signedByteTupleToReg v)) I guess this will show up as a proof obligation when proving correctness of the MMIO compiler (in |
Cool, I'll make one then.
I just made my own I haven't edited the existing MMIO.v yet, but since MMIOReadOK would be just |
Aah I see, so you just push the problem up to the next layer... 😉 |
Yes, I've done one for a small firmware program that interfacing with a peripheral block that takes a single-register input via an MMIO write and after an unknown but upper-bounded number of cycles signals it's done via a flag in a separate "status" register which indicates the output register is now safe to read. My example specification for what the circuit actually does is just "add 1 to the input", but you can plug in any word -> word function.The firmware program writes the value, reads the status register until it sees the "done" flag, and then reads the output register and returns the output. The spec of the program is that, assuming the peripheral block behaves as expected, the program will return the correct result according to the circuit spec. I have to reason about the hardware guarantees in program logic to prove that the firmware program's loop terminates. The code is here (in a branch on my fork, can't be merged to main branch yet because it's on top of the changes I've made to Possible files of interest: The signalling structure here is designed to be a minimal imitation of the signalling for an AES block I'm trying to do some firmware verification for. I've got some bedrock2 code + proofs for that (which is in the main branch), although I'll need to change some things so it works with the compiler proofs (I'm using the small example circuit to figure out how the core logical structure works without dealing with the many registers and more complicated logic of the full block). That example would be very non-trivial, and I am pretty confident that it will work given that the smaller one does.
No, I successfully use the trick on top of the proposed In the software proof, you end up with two goals: 1) there exists some valid read and 2) given a read value and the fact that it was valid, your postcondition holds. You can see that in the helper lemma for read interactions: The "valid read" condition for my state-machine reasoning is, in prose: "given the trace so far In goal (2) ("postcondition holds for all valid reads") you already have the guarantee in context that the read was valid -- this allows you to use things like "I know my circuit will not say it's still busy for more than N cycles" to prove loop termination etc. Sorry for the essay, just wanted to talk you through the details of how I've set this up! |
Thanks for the details, so I'll try to summarize my confusion and how it got resolved: I was assuming you're doing 1), but in fact you're doing 2):
|
I think the overall approach here makes sense, and I'm fine with merging this parameter. I'm not sure whether exposing the "exists and forall" structure to program logic proofs is the most ergonomic approach (but it's not being merged to bedrock2, so idc). Intead I imagine we could prove a lemma about your extspec saying that if it holds for some parameters then the postcondition parameter must be non-vacuous, use a context variable representing lemma to prove totality of riscv/bedrock2 semantics, and leave software proofs with more natural goals... |
This seems like a good idea; I'm not sure I know how to implement it but would be interested in this kind of layer. In my particular use case, the existential goals are pretty easy to dispatch. |
That sounds interesting, but how to make sure that the extspec satisfies this lemma? The only way I can think of would be to include in the extspec that existential that we'd like to get rid of... |
Let's say I'm trying to prove termination for a loop that looks like this:
Based on the hardware specification, I know that the loop will not run forever because the "done" bit in the status register will eventually be set to 1 (guaranteed after a certain maximum number of cycles, which translates to a maximum number of MMIO reads). I've gotten the proof working by writing my
ext_spec
as essentiallytrace-allowed-by-hardware-spec -> postcondition
. However, looking at the examples in the repo (mainlyFE310CSemantics.v
), theext_spec
s don't have any preconditions. I'm wondering if there's a better way to do what I want to do here, or if I'm violating a convention and setting myself up for later problems.The text was updated successfully, but these errors were encountered: