-
Notifications
You must be signed in to change notification settings - Fork 258
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Allocation soundness issue #4844
Comments
I think this issue could be solved with two things:
method Ooops()
ensures false
{
var o := new O();
assert WasNotAllocatedBefore(o);
label afterAlloc:
o.x := 1;
assert WasNotAllocatedBefore@afterAlloc(o); // No reads clause makes this function have the same value for both heaps!
// Contradiction
}
class O {
var x: int
constructor() {}
}
{:assume_allocated false}
twostate predicate WasNotAllocatedBefore(o: O)
{ !old(allocated(o)) } Put together, we would have the following code that would be sound: method Finallyyyyy()
{
var o := new O();
assert WasNotAllocatedBefore(o);
label afterAlloc:
o.x := 1;
assert WasNotAllocatedBefore@afterAlloc(o); // Error, because it reads the allocated bit which was modified for the first heap.
}
class O {
var x: int
constructor() {}
}
{:assume_allocated false}
twostate predicate WasNotAllocatedBefore(o: O)
reads o`allocated // Need a reads clause like this
{ !old(allocated(o)) } |
@MikaelMayer Thanks for the bug report. It is indeed a soundness issue. The above analysis of the issue is not correct, however. Let me first comment on that, and also comment on the Checking allocatedness at call sitesDafny does check, at call sites, that non-
As another example, if you add another parameter to the two-state predicate (like In general, whenever a reference is dereferenced outside the current state, Dafny checks that the object really was allocated. Functions that read the stateIt is difficult to work with The The completeness issue is that a call The class Context {
var i: int
method DoIt()
modifies this
{
i := 16;
}
method Monitor(test: Context ~> bool)
requires test.requires(this)
modifies this
ensures test.requires(this) ==> test(this)
decreases *
{
DoIt();
if !test(this) { // error: precondition violation
Monitor(test);
return;
}
}
}
function DoTest(c: Context): bool
reads c
{
c.i == 15
}
method Test(c: Context)
modifies c
decreases *
{
c.i := 15;
c.Monitor(DoTest);
} In this example, Dafny correctly complains that calling So, I think the problem with this example application is that |
Back to the soundness issue detected by the Root cause and fixThe root cause is that the Consequence Axiom for the two-state predicate (forall $prevHeap: Heap, $Heap: Heap, n#0: int, o#0: ref ::
{ _module.__default.AllocationSoundness($prevHeap, $Heap, n#0, o#0) }
_module.__default.AllocationSoundness#canCall($prevHeap, $Heap, n#0, o#0)
|| (1 < $FunctionContextHeight
&&
$IsGoodHeap($prevHeap)
&& $IsGoodHeap($Heap)
&& $HeapSucc($prevHeap, $Heap)
&& LitInt(0) <= n#0
&& $Is(o#0, Tclass._module.O()))
==> $IsAllocBox($Box(o#0), Tclass._module.O(), $prevHeap)) It says that, from rather general assumptions, and certainly no allocatedness assumptions about To fix the issue, the long part of the antecedent should also include $IsAlloc(o#0, Tclass._module.O(), $prevHeap) In fact, such an antecedent should be included for every non- Why so difficult to get bit by the bug?The soundness bug is not easy to trigger. The Boogie encoding of the Dafny statement assert AllocationSoundness(x); is something like this (I'm leaving off some irrelevant parts): assert $IsAlloc(x#0, Tclass._module.O(), old($Heap)); // (*)
...
assume AllocationSoundness#canCall(old($Heap), $Heap, x#0);
assert {:subsumption 0}
AllocationSoundness#canCall(old($Heap), $Heap, x#0)
==> AllocationSoundness(old($Heap), $Heap, x#0) || Lit(true);
assume AllocationSoundness(old($Heap), $Heap, x#0); The first assertion (which I marked with ()) is the one that checks the actual argument to be allocated in the state We want all our axioms to be sound, even if matching patterns (triggers) are ignored. But since Z3 does use matching patterns, then how come it's able to use the Consequence Axiom whose trigger mentions The first thing is that assertion The second mysterious thing is that the second assertion I show above matters. This by itself contains two mysteries. One is that, with the form The fixAgain, the cause of the soundness bug is the missing antecedent about non- The bug report also shows that, even if the appropriate conditions are checked at the call site, there may be a discrepancy in the corresponding axiom. The |
Wow, thanks for having written the time for such a detailed analysis.
Indeed, I see that the two variations show that the check was being performed. Good finding! You highlighted that the proof of
I would have loved to have ~> mean total in my case, and the most general arrow function be ~~>. That would have solved the above problem. class Context {
var i: int
method DoIt()
modifies this
{
i := 16;
}
method Monitor(test: (Context, int) ~> bool)
requires test.requires(this, 1000)
modifies this
ensures test.requires(this, 1000) ==> test(this)
decreases *
{
DoIt();
if !test(this, 1000) { // error: precondition violation
Monitor(test);
return;
}
}
}
function DoTest(c: Context, divider: int): bool
reads c
requires divider != 0
{
c.i/divider == 15
}
method Test(c: Context)
modifies c
decreases *
{
c.i := 15;
c.Monitor(DoTest);
} Above, the precondition of
I agree in all cases that we should add this requirement for every non-new parameter of the two-state predicate.
I see. So Z3 can use for triggers things that appear after an expression somehow. That makes sense since using this trigger makes it possible to prove the point. I'd love to see a simpler version indeed with only cancall. Extra commentI would like us to think carefully of the following case. Mentally, in Dafny, every boolean expression can be refactored to a predicate or twostate predicate (except expressions referring to three different heaps). However, the postcondition of the method
Of course, the fix is to strengthen the postcondition and state that |
A reply regarding the example in "Extra comment": Yes, you can extract that postcondition into a two-state predicate: twostate predicate P(new o2: O)
reads o2
{
old(allocated(o2)) && old(o2.x) == o2.x
}
method MaybeDuplicate(o: O, create: bool) returns (o2: O)
ensures !create ==> P(o2) The |
Wow, that's great to know, thanks! I hadn't learn about "new" in parameters. Wonderful. |
…arguments are allocated in the previous heap (#5142) This PR fixes #4844 according to Rustan's remarks Fixes #5132 as well by modifying the error message to be more explicit about the possibilities <small>By submitting this pull request, I confirm that my contribution is made under the terms of the [MIT license](https://github.com/dafny-lang/dafny/blob/master/LICENSE.txt).</small> --------- Co-authored-by: Rustan Leino <leino@amazon.com>
Dafny version
latest-nightly
Code to produce this issue
Command to run and resulting output
What happened?
Twostate predicates can refer to non-allocated state. However, they assume everything is allocated, so we can extract this assumption and prove that objects in their old state were not allocated.
Even if we did not have this assumption in twostate predicate, they could call predicates that would in turn assume allocatedness.
Or lemmas that could assume allocatedness.
Really it all boils down to alloc being a field and assuming it in ghost contexts when the ghost context could actually refer to an object before it was allocated (through
old@()
), which is not possible for compiled contexts.My advice is that that every time we call a twostate function or twostate lemma in a method, we need to check
allocated()
for all their arguments in the pre-state.In the example above, the proof obligation
would have caught the issue upfront.
In the long-term, we might want to have twostate functions, ghost functions and lemmas that would not assume allocatedness in any way; however, they become a new color of functions/lemmas as they should not be able to call other ghost functions or lemmas that assume allocatedness without proving the allocatedness of all their arguments.
Still, they would make it possible to implement the following currently impossible program in Dafny
To solve the above issue, we could introduce quantification over any heap:
Note that in the above case, if we removed the argument for the test, we would still be able to write this as this is always allocated at the beginning of methods
and that would make our test to work
What type of operating system are you experiencing the problem on?
Windows
The text was updated successfully, but these errors were encountered: