Description
CBMC version: develop
Operating system: Mac OS 10.15.7
I know it's recommended not to use __CPROVER_r_ok
within __CPROVER_assume
, but it's not enforced -- no error from CBMC.
Some projects (e.g. throughout s2n) use this, and the point of this test case is to demonstrate that CBMC would accept bad programs when __CPROVER_r_ok
is used in __CPROVER_assume
, and give a false guarantee of correctness even with all of the pointer-checking flags enabled.
Test case:
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
bool validate(const char *data, bool allocated)
{
bool check_1 = (data == NULL) ==> (!allocated);
bool check_2 = allocated ==> __CPROVER_r_ok(data, 128);
return check_1 && check_2;
}
void main()
{
char *data;
bool allocated;
data = !allocated ? NULL : malloc(1);
__CPROVER_assume(validate(data, allocated));
// same as `validate`, but inlined here to see which assertion fails
assert((data == NULL) ==> (!allocated));
assert(allocated ==> __CPROVER_r_ok(data, 128));
}
Exact command line resulting in the issue:
cbmc --malloc-may-fail --malloc-fail-null --pointer-primitive-check --bounds-check --pointer-check --pointer-overflow-check test.c
What behaviour did you expect:
The second assert
should fail because data
is malloc
ed with 1 byte and shouldn't be readable up to 128 bytes.
What happened instead:
CBMC reports that the program is valid -- all implicit and explicit assertions pass. I have used all the CBMC -check
flags that I thought could be relevant in this context.
Additional information:
With the --smt2
flag, CBMC does report an assertion failure at the last line of main
and also within the __CPROVER_r_ok
check, as expected.
This bug is related to, but is different from, #5952. There we actually read the same length as we allocated, so the failure from --smt2
is unexpected. In this report, success from the SAT-based solver is unexpected.
Activity
SaswatPadhi commentedon Mar 19, 2021
A simpler example:
No warnings / errors and verification succeeds with the following cmdline:
Full output from CBMC
[-]Soundness bug with use of memory primitives in `assume`[/-][+]Memory primitives in `assume` are unchecked[/+]TGWDB commentedon Apr 20, 2021
I've done some exploration of this issue and there are two different things interacting that cause the unexpected behaviour. One appears to be a misunderstanding about how
cbmc
checks properties, and the other is (probably) a bug in the--smt2
option.Part 1 - Misunderstanding CBMC
To explain the misunderstanding let's consider the simpler example:
Now there are several commands we can run to show interesting output (somewhat simplified from the above). Let's use the following to illustrate:
which produces the following output (I've added cat for illustration):
The key point here is that the assertion on line 12 passes. This appears to be sufficient to illustrate the misunderstanding with
cbmc
. To clarify, consifer the following that is the same except with an additional assertion that should always fail added:Observe that here the assertion on line 13 does not fail. This is because of how
cbmc
performs checks.To understand this, let us consider the
withfail.c
example:The internals of
cbmc
operate as follows.data
that has no known value.data
obtains the result ofmalloc(1)
which is assumed to succeed (note that some flags can allow failures here, but it does not matter for this part).__CPROVER_r_ok(data, 128)
attempts to refine the possible values ofdata
. However, since there is no value set fordata
that is consistent with lines 9 and 11, this path becomes unreachable.cbmc
assumes that unreachable checks pass (i.e. they succeed here).Note that line 12 can be commented out and line 13 will still succeed.
To conclude the misunderstanding part, since the path becomes unreachable any checks are considered to succeed by
cbmc
and this is why line 12 (in bothsimple.c
andwithfail.c
succeed.Part 2 - Different smt2 Behaviour
The second aspect of this issue is that there is different behaviour when the
--smt2
flag is used. In particular there appears to be a bug in the SMT translation for the__CPROVER_r_ok
check and the relation with thedata
pointer on line 11. This appears to be related to #5952 although it is unclear for the moment if they are exactly the same problem.Some experiments show that the smt2 outfile for the examples does indded include
but I have not checked if the details sent to Z3 are different to the outfile.
That said, the path to addressing them has the same question: is this an urgent bug to explore and fix considering the upcoming work on rewriting the SMT backend?
SaswatPadhi commentedon Apr 20, 2021
Thanks for looking into this, @TGWDB.
Regarding part 1, we understand that that incorrect use of
__CPROVER_r_ok
in assume context would lead toassume(false);
any other check after that would vacuously succeed. The fact that these primitives don't work as expected in assume context -- don't really make an allocation have a desired size, is not intuitive though and so in large projects it's easy to end up with vacuous proofs.The expectation here is to raise a "warning" and infrom users that these primitives could easily lead to vacuous proofs when used in assume context. May be @feliperodri and @mww-aws could add more.
Regarding part 2, no fixing the SMT issue with the old backend is not urgent. It probably is the same issue as #5952, as you mentioned, and that's on my todo list, but it's not urgent.
tautschnig commentedon Apr 20, 2021
Selectively quoting:
I'm not quite following why the example is a case of unintuitive semantics -- it seems
__CPROVER_r_ok(data, 128)
is rather obviously false? I thought that--pointer-primitive-check
was the approach to identify cases where the semantics may be surprising.Isn't it really coverage checking that you need?
SaswatPadhi commentedon Apr 20, 2021
But
--pointer-primitive-check
doesn't raise any warning or error in this case either (see the second comment). So I am not sure in what way is it more helpful than not using it, in this case.That's one approach yes ... but we don't yet have an automated coverage check. I also think it might be hard to enforce such an automated check because the acceptable coverage across harnesses varies a lot -- may be @feliperodri can comment.
feliperodri commentedon Apr 20, 2021
@tautschnig not only for coverage checking, but also to check for valid pointers.
--pointer-primitive-check
helps us to identify when we try to use memory primitives with invalid pointers (i.e., neither NULL or allocated). However, we want to warn users that using memory primitives in assumptions could lead to unreachable traces. In the past, users believed the example above succeeds because there are no failures, when in fact the assertions are unreachable. In this case, there is absolutely nothing in the standard CBMC output that indicates unreachable checks.tautschnig commentedon Apr 21, 2021
Unless coverage checking is enabled, there never is information about unreachable checks. That's why coverage checking is (at least: currently) necessary. As much as I do agree that the memory primitives have their surprising pitfalls: I'm not sure why they'd deserve preferential treatment here. The example that @SaswatPadhi posted at the beginning of this conversation is a really good case here: one can easily construct arbitrarily complex assumptions, and, yes, some of them will result in vacuous proofs. Some of them might contain memory primitives, but that's not at all a necessary condition.
From my point of view, there are two possible routes forward. Further suggestions would be much appreciated!
assert(__CPROVER_{r,w}_ok(...))
.feliperodri commentedon Apr 21, 2021
They could be very useful when dealing with pointers and we often tried to use them, but fell in their pitfalls. So, targeting them here wouldn't solve the vacuity problem, but it'd at least help users to not apply them in the wrong context unintentionally.
I think this is unnecessary for now, because we already perform coverage checking in proof harnesses.
I like this idea. This would make clear to proof writers "be aware where you're using these primitives". @SaswatPadhi @mww-aws @markrtuttle any thoughts?
SaswatPadhi commentedon Apr 21, 2021
+1. I like it too. This is exactly what I was trying to say when I mentioned that we should warn users about using these primitives in
assume
context.TGWDB commentedon May 7, 2021
I propose this issue is closed as there does not appear to be a true bug here. Also since the concern of vacuous proofs is now in #6057 and the SMT aspect is in #5952 (or the upcoming SMT backend rework).
SaswatPadhi commentedon May 7, 2021
Sure, I am closing this issue. A fix for #6057 would also check for vacuity due to usage of memory primitives in assume context.