Skip to content

__CPROVER_w_ok has weird behaviour #5194

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

Closed
danielsn opened this issue Nov 20, 2019 · 29 comments
Closed

__CPROVER_w_ok has weird behaviour #5194

danielsn opened this issue Nov 20, 2019 · 29 comments
Labels
aws Bugs or features of importance to AWS CBMC users

Comments

@danielsn
Copy link
Contributor

Since y > x, it should be true that !__CPROVER_w_ok(a, y) is always false. However, if you assert this fact, you get an assertion violation.

#include<assert.h>
#include<stdlib.h>
#include<stdint.h>

int main() {
  size_t x;
  size_t y;
  uint8_t* a;

  __CPROVER_assume(x > 0);
  __CPROVER_assume(y > x);

  a = malloc(sizeof(*(a)) * (x));

  assert(!__CPROVER_w_ok(a, y));
}
~/temp $ cbmc *.c --trace --pointer-check --bounds-check
CBMC version 5.12 (cbmc-5.12-d8598f8-1008-gfab409d18) 64-bit x86_64 macos
Parsing test.c
Converting
Type-checking test
Generating GOTO Program
Adding CPROVER library (x86_64)
Removal of function pointers and virtual functions
Generic Property Instrumentation
Running with 8 object bits, 56 offset bits (default)
Starting Bounded Model Checking
size of program expression: 70 steps
simple slicing removed 5 assignments
Generated 1 VCC(s), 1 remaining after simplification
Passing problem to propositional reduction
converting SSA
Running propositional reduction
Post-processing
Solving with MiniSAT 2.2.1 with simplifier
874 variables, 2035 clauses
SAT checker: instance is SATISFIABLE
Runtime decision procedure: 0.00674373s
Building error trace

** Results:
test.c function main
[main.assertion.1] line 15 assertion !__CPROVER_w_ok(a, y): FAILURE

Trace for main.assertion.1:

State 21 file test.c function main line 6 thread 0
----------------------------------------------------
  x=35235878141952ul (00000000 00000000 00100000 00001011 11111110 00000000 00000000 00000000)

State 22 file test.c function main line 7 thread 0
----------------------------------------------------
  y=36028814198833152ul (00000000 10000000 00000000 00000100 00000000 00000000 00000000 00000000)

State 23 file test.c function main line 8 thread 0
----------------------------------------------------
  a=((uint8_t *)NULL) (00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000)

Assumption:
  file test.c line 10 function main
  x > (unsigned long int)0

Assumption:
  file test.c line 11 function main
  y > x

State 29 file test.c function main line 13 thread 0
----------------------------------------------------
  malloc_size=35235878141952ul (00000000 00000000 00100000 00001011 11111110 00000000 00000000 00000000)

State 48 file test.c function main line 13 thread 0
----------------------------------------------------
  a=dynamic_object1 (00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000)

Violated property:
  file test.c function main line 15 thread 0
  assertion !__CPROVER_w_ok(a, y)
  !((signed long int)(signed long int)!(!((FALSE || !(POINTER_OBJECT((const void *)a) == POINTER_OBJECT(NULL))) && !IS_INVALID_POINTER((const void *)a) && (FALSE || !(POINTER_OBJECT((const void *)a) == POINTER_OBJECT(__CPROVER_deallocated))) && (FALSE || !(POINTER_OBJECT((const void *)a) == POINTER_OBJECT(__CPROVER_dead_object))) && (FALSE || (POINTER_OBJECT((const void *)a) == POINTER_OBJECT(__CPROVER_malloc_object) ==> !(POINTER_OFFSET((const void *)a) < 0l || (size_t)POINTER_OFFSET((const void *)a) + y > __CPROVER_malloc_size))) && (FALSE || (!IS_DYNAMIC_OBJECT((const void *)a) ==> !(POINTER_OFFSET((const void *)a) < 0l || (size_t)POINTER_OFFSET((const void *)a) + y > OBJECT_SIZE((const void *)a)))) && (POINTER_OBJECT(NULL) == POINTER_OBJECT((const void *)a) && NULL != (const void *)a ==> FALSE))) != 0l)



** 1 of 1 failed (2 iterations)
VERIFICATION FAILED
~/temp $ cbmc --version
5.12 (cbmc-5.12-d8598f8-1008-gfab409d18)
~/temp $ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.6
BuildVersion:	18G1012
@danielsn danielsn added the aws Bugs or features of importance to AWS CBMC users label Nov 20, 2019
@danielsn
Copy link
Contributor Author

Most likely related to the issue in https://github.com/danielsn/s2n/tree/cbmc_issue_5194 on the s2n/tests/cbmc/proofs/s2n_stuffer_init proof
@tautschnig

@thk123
Copy link
Contributor

thk123 commented Jan 28, 2020

This is tracked internally by ADA-450

@danpoe
Copy link
Contributor

danpoe commented Jan 31, 2020

This behavior is due to how bounds checking for dynamically allocated memory is implemented in cbmc.

The model for malloc() in cbmc looks essentially as follows:

void *malloc(__CPROVER_size_t malloc_size)
{
  void *malloc_res = __CPROVER_allocate(malloc_size, 0);

  if(nondet_bool()) {
    __CPROVER_malloc_object = malloc_res;
    __CPROVER_malloc_size = malloc_size;
  }

  return malloc_res;
}

The nondeterministic switch controls whether the address and size of the memory block allocated in this particular invocation of malloc() are recorded.

The condition __CPROVER_r_ok(a, size) checks the following (simplified):

POINTER_OBJECT(a) == POINTER_OBJECT(__CPROVER_malloc_object) ==>
POINTER_OFFSET(a) + size <= __CPROVER_malloc_size

To illustrate how the bounds checking works, consider the following example program:

int main()
{
  char *p1 = malloc(1);
  char *p2 = malloc(2);

  assert(__CPROVER_r_ok(p1, 2));
}

Here the assertion should fail. In the approach outlined above it indeed can, as one can choose true for nondet_bool() in the first call to malloc(), and false for nondet_bool() in the second call to malloc(). Thus, the object address and size of the first call to malloc() are recorded in __CPROVER_malloc_object and __CPROVER_malloc_size respectively. Thus, the premise of the implication above is true, while the conclusion is false, hence __CPROVER_r_ok() is false.

This approach for bounds checking is also the reason for why there is (almost) always a trace for which !__CPROVER_r_ok(a, size) is false. The negated condition checks the following (simplified):

POINTER_OBJECT(a) == POINTER_OBJECT(__CPROVER_malloc_object) &&
POINTER_OFFSET(a) + size > __CPROVER_malloc_size

If choosing false for nondet_bool() for the malloc() call that allocated the memory pointed to by a, then the first conjunct and thus the whole condition will be false.

In summary, this behavior is not a bug but due to how cbmc implements bounds checking for dynamically allocated memory. Also while the negative version !__CPROVER_r_ok() is imprecise, the positive version __CPROVER_r_ok() is precise. Maybe in the future we should remove __CPROVER_r_ok() and provide a primitive __CPROVER_assert_r_ok() instead.

@karkhaz
Copy link
Collaborator

karkhaz commented Feb 4, 2020

Here's another example that @danielsn and I came up with:

#include <stdlib.h>

int main()
{
  int *x = malloc(2), *y = malloc(2);
  assert(__CPROVER_r_ok(x, 3) == __CPROVER_r_ok(y, 3));
}

This assertion fails. It also fails if you change the == into !=.

@danpoe
Copy link
Contributor

danpoe commented Feb 4, 2020

This behavior is due to how __CPROVER_r_ok() is implemented. At present, the only useful way to use the primitive is as assert(__CPROVER_r_ok(x, size)). That's why we should probably at some point replace __CPROVER_r_ok() with a new primitive __CPROVER_assert_r_ok() which directly asserts the condition (instead of just returning a boolean).

@danielsn
Copy link
Contributor Author

danielsn commented Feb 6, 2020

The example I mentioned today:

#include <assert.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>


int main() {
    char* foo;
    __CPROVER_assume(__CPROVER_r_ok(foo, 10));
    foo[20];
    return 1;
}
~/temp $ cbmc --pointer-check --bounds-check test.c
CBMC version 5.12 (cbmc-5.12-d8598f8-1407-ga7f5ebd05-dirty) 64-bit x86_64 macos
Parsing test.c
Converting
Type-checking test
Generating GOTO Program
Adding CPROVER library (x86_64)
Removal of function pointers and virtual functions
Generic Property Instrumentation
Running with 8 object bits, 56 offset bits (default)
Starting Bounded Model Checking
size of program expression: 38 steps
simple slicing removed 5 assignments
Generated 1 VCC(s), 1 remaining after simplification
Passing problem to propositional reduction
converting SSA
Running propositional reduction
Post-processing
Solving with MiniSAT 2.2.1 with simplifier
89 variables, 9 clauses
SAT checker inconsistent: instance is UNSATISFIABLE
Runtime decision procedure: 0.00186795s

** Results:
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/stdio.h function __sputc
[__sputc.pointer_dereference.1] line 265 dereference failure: pointer NULL in _p->_w: SUCCESS
[__sputc.pointer_dereference.2] line 265 dereference failure: pointer invalid in _p->_w: SUCCESS
[__sputc.pointer_dereference.3] line 265 dereference failure: deallocated dynamic object in _p->_w: SUCCESS
[__sputc.pointer_dereference.4] line 265 dereference failure: dead object in _p->_w: SUCCESS
[__sputc.pointer_dereference.5] line 265 dereference failure: pointer outside dynamic object bounds in _p->_w: SUCCESS
[__sputc.pointer_dereference.6] line 265 dereference failure: pointer outside object bounds in _p->_w: SUCCESS
[__sputc.pointer_dereference.7] line 265 dereference failure: invalid integer address in _p->_w: SUCCESS
[__sputc.pointer_dereference.8] line 265 dereference failure: pointer NULL in _p->_w: SUCCESS
[__sputc.pointer_dereference.9] line 265 dereference failure: pointer invalid in _p->_w: SUCCESS
[__sputc.pointer_dereference.10] line 265 dereference failure: deallocated dynamic object in _p->_w: SUCCESS
[__sputc.pointer_dereference.11] line 265 dereference failure: dead object in _p->_w: SUCCESS
[__sputc.pointer_dereference.12] line 265 dereference failure: pointer outside dynamic object bounds in _p->_w: SUCCESS
[__sputc.pointer_dereference.13] line 265 dereference failure: pointer outside object bounds in _p->_w: SUCCESS
[__sputc.pointer_dereference.14] line 265 dereference failure: invalid integer address in _p->_w: SUCCESS
[__sputc.pointer_dereference.15] line 265 dereference failure: pointer NULL in _p->_w: SUCCESS
[__sputc.pointer_dereference.16] line 265 dereference failure: pointer invalid in _p->_w: SUCCESS
[__sputc.pointer_dereference.17] line 265 dereference failure: deallocated dynamic object in _p->_w: SUCCESS
[__sputc.pointer_dereference.18] line 265 dereference failure: dead object in _p->_w: SUCCESS
[__sputc.pointer_dereference.19] line 265 dereference failure: pointer outside dynamic object bounds in _p->_w: SUCCESS
[__sputc.pointer_dereference.20] line 265 dereference failure: pointer outside object bounds in _p->_w: SUCCESS
[__sputc.pointer_dereference.21] line 265 dereference failure: invalid integer address in _p->_w: SUCCESS
[__sputc.pointer_dereference.22] line 265 dereference failure: pointer outside dynamic object bounds in _p->_lbfsize: SUCCESS
[__sputc.pointer_dereference.23] line 265 dereference failure: pointer outside object bounds in _p->_lbfsize: SUCCESS
[__sputc.pointer_dereference.24] line 265 dereference failure: invalid integer address in _p->_lbfsize: SUCCESS
[__sputc.pointer_dereference.25] line 266 dereference failure: pointer NULL in _p->_p: SUCCESS
[__sputc.pointer_dereference.26] line 266 dereference failure: pointer invalid in _p->_p: SUCCESS
[__sputc.pointer_dereference.27] line 266 dereference failure: deallocated dynamic object in _p->_p: SUCCESS
[__sputc.pointer_dereference.28] line 266 dereference failure: dead object in _p->_p: SUCCESS
[__sputc.pointer_dereference.29] line 266 dereference failure: pointer outside dynamic object bounds in _p->_p: SUCCESS
[__sputc.pointer_dereference.30] line 266 dereference failure: pointer outside object bounds in _p->_p: SUCCESS
[__sputc.pointer_dereference.31] line 266 dereference failure: invalid integer address in _p->_p: SUCCESS
[__sputc.pointer_dereference.32] line 266 dereference failure: pointer NULL in *tmp_post: SUCCESS
[__sputc.pointer_dereference.33] line 266 dereference failure: pointer invalid in *tmp_post: SUCCESS
[__sputc.pointer_dereference.34] line 266 dereference failure: deallocated dynamic object in *tmp_post: SUCCESS
[__sputc.pointer_dereference.35] line 266 dereference failure: dead object in *tmp_post: SUCCESS
[__sputc.pointer_dereference.36] line 266 dereference failure: pointer outside dynamic object bounds in *tmp_post: SUCCESS
[__sputc.pointer_dereference.37] line 266 dereference failure: pointer outside object bounds in *tmp_post: SUCCESS
[__sputc.pointer_dereference.38] line 266 dereference failure: invalid integer address in *tmp_post: SUCCESS
[__sputc.pointer_dereference.39] line 266 dereference failure: pointer NULL in *tmp_post: SUCCESS
[__sputc.pointer_dereference.40] line 266 dereference failure: pointer invalid in *tmp_post: SUCCESS
[__sputc.pointer_dereference.41] line 266 dereference failure: deallocated dynamic object in *tmp_post: SUCCESS
[__sputc.pointer_dereference.42] line 266 dereference failure: dead object in *tmp_post: SUCCESS
[__sputc.pointer_dereference.43] line 266 dereference failure: pointer outside dynamic object bounds in *tmp_post: SUCCESS
[__sputc.pointer_dereference.44] line 266 dereference failure: pointer outside object bounds in *tmp_post: SUCCESS
[__sputc.pointer_dereference.45] line 266 dereference failure: invalid integer address in *tmp_post: SUCCESS

test.c function main
[main.pointer_dereference.1] line 11 dereference failure: pointer uninitialized in foo[(signed long int)20]: SUCCESS

** 0 of 46 failed (1 iterations)
VERIFICATION SUCCESSFUL

@danpoe
Copy link
Contributor

danpoe commented Feb 11, 2020

Thanks for the example. After looking further into this, it turns out that all uses of __CPROVER_r_ok(p, size) other than assert(__CPROVER_r_ok(p, size)) are not expected to work at the moment due to the way it is implemented. In most cases, it should be possible to model the behavior using other CPROVER primitives, e.g. for the provided example:

#include <stdlib.h>

void main()
{
  size_t size;
  __CPROVER_assume(size >= 10);
  char *foo = malloc(size);
  __CPROVER_assume(foo != NULL);

  foo[20];
}

@mww-aws
Copy link
Contributor

mww-aws commented Feb 12, 2020

It is obvious this is how it is currently implemented, so whether you consider it a bug or not depends on whether you consider design issues.

To me, this is not a useful definition in full generality. What is required here is a mechanism for getting the size of an allocated pointer. Rather than __CPROVER_malloc_size, we need __CPROVER_malloc_size(a) where a is a pointer in the program. If a is not an allocated pointer, this call should lead to an error. The metadata for size must exist in CBMC already. What we need is a mechanism to pull it out. Then we could drop the global variables and do a proper comparison:
POINTER_OFFSET(a) + size > __CPROVER_malloc_size(a)

There are many useful problems that could be solved by supporting such a macro, including using the __CPROVER_r_ok in negative contexts (it is not just assumptions, it is under any negation where it will fail, so the renaming trick is not sufficient).

@karkhaz
Copy link
Collaborator

karkhaz commented Feb 13, 2020

@danpoe is this something that somebody at Diffblue could work on implementing, or give me some pointers on how to get started? This is feature is vital to us–we'll need a predicate that does the right thing when used in an assume (so __CPROVER_assert_r_ok() wouldn't work, we do need something that returns a boolean).

So the required behavior would be: if we assume that a pointer p points to a valid object, then CBMC should actually mint that object and then use it for future accesses to *p. If we assert that p points to a valid object, then CBMC should turn that into a disjunction over all of the objects that have been allocated during symbolic execution.

The broader context is the function contracts work: we need to be able to say that a function parameter is backed by valid memory in a function precondition. This needs to work in the case where we're checking a function independently of its calling context; since nobody has called the function, there isn't a real object that was passed into the function, so we need to assume that the object exists in the function precondition.

karkhaz added a commit to karkhaz/aws-c-common that referenced this issue Feb 27, 2020
This is while we work to resolve an issue with CBMC, described at
diffblue/cbmc#5194
karkhaz added a commit to karkhaz/amazon-freertos that referenced this issue Feb 27, 2020
This commit attempts to preserve the functionality of the memory checks
in is_valid_IotResponseHandle until the issue in
diffblue/cbmc#5194 is sorted out. This commit
is needed because is_valid_IotResponseHandle uses the __CPROVER_r_ok and
__CPROVER_w_ok macros, and is_valid_IotResponseHandle itself is used
inside an assume statement, triggering the issue mentioned in that bug.
@tautschnig
Copy link
Collaborator

@danpoe @karkhaz @danielsn Should this one be closed now that #5247 has been merged? Though perhaps this should be done with an additional regression test that actually uses assumes. How about this one, which would not previously have worked:

#include <stdlib.h>

int main() {
    unsigned length;
    char* foo = malloc(length);
    __CPROVER_assume(__CPROVER_r_ok(foo, 21));
    char x = foo[20];
    return 1;
}

@mww-aws
Copy link
Contributor

mww-aws commented Mar 2, 2020 via email

@karkhaz
Copy link
Collaborator

karkhaz commented Mar 2, 2020

@tautschnig @mww-aws let's not close this quite yet. I haven't had a chance to play with #5247 yet, but @danpoe mentioned that it's only a partial fix.

I haven't studied how it works, but it does work on my example with the two r_oks in an assert, but it does not work on @danielsn 's example that accesses a 10-length buffer at index 20.

danielsn pushed a commit to karkhaz/aws-c-common that referenced this issue Mar 2, 2020
This is while we work to resolve an issue with CBMC, described at
diffblue/cbmc#5194
@karkhaz
Copy link
Collaborator

karkhaz commented Mar 4, 2020

I had a chat with @danpoe, writing down notes for the record:

  • This bug apparently only occurs when we assume r_ok or w_ok on pointers that had not previously been initialized. This is because when a pointer is defined without being initialized, CBMC marks it as being INVALID. Any subsequent assignment (including to NULL) removes this INVALID flag. This is implemented in local_bitvector_analysist. There is a predicate that we can use during symex to check for this: __CPROVER_is_invalid. So using r_ok says that the pointer is backed by memory, yet the pointer is INVALID---these are contradictory constraints.

Implementation idea: if a pointer is INVALID during symex when we reach an r_ok, then synthesize a malloc of the right length at that point.

  • This deals with the case where we use r_ok in a harness to constrain an uninitialized pointer
  • This is also fine for preconditions of functions that get called from a calling context, since the calling context will have initialized the argument, so the argument will not be INVALID.

Other ideas: @danpoe had proposed creating an assume_r_ok( <boolean exp> ) macro that does the right thing. This wouldn't work for us, since we often want to have several r_ok calls, joined with logical operations, all inside __CPROVER_assume.

danielsn pushed a commit to karkhaz/aws-c-common that referenced this issue Mar 10, 2020
This is while we work to resolve an issue with CBMC, described at
diffblue/cbmc#5194
markrtuttle pushed a commit to karkhaz/amazon-freertos that referenced this issue Mar 11, 2020
This commit attempts to preserve the functionality of the memory
checks in is_valid_IotResponseHandle until the issue in
diffblue/cbmc#5194 is sorted out. This
commit is needed because is_valid_IotRequestHandle and
is_valid_IotResponseHandle use the __CPROVER_r_ok and __CPROVER_w_ok
macros, and they are used inside an assume statement, triggering the
issue mentioned in that bug.
karkhaz added a commit to karkhaz/amazon-freertos that referenced this issue Mar 11, 2020
This commit attempts to preserve the functionality of the memory
checks in is_valid_IotResponseHandle until the issue in
diffblue/cbmc#5194 is sorted out. This
commit is needed because is_valid_IotRequestHandle and
is_valid_IotResponseHandle use the __CPROVER_r_ok and __CPROVER_w_ok
macros, and they are used inside an assume statement, triggering the
issue mentioned in that bug.
justinboswell pushed a commit to awslabs/aws-c-common that referenced this issue Mar 11, 2020
* Temporarily disable the __CPROVER_{r,w}_ok macros

This is while we work to resolve an issue with CBMC, described at
diffblue/cbmc#5194

* fix aws_array_list_back

* fix aws_array_list_front

* fix aws_array_list_get_at

* fix aws_byte_cursor_read_u8 proof

* fix CBMC proof for aws_array_list_set_at

* fix aws_byte_buf_write_from_whole_cursor CBMC proof, and minor issue in aws_byte_buf_write

* fix CBMC proof for aws_array_list_push_back

* fix byte_cursor read proofs

Co-authored-by: Daniel Schwartz-Narbonne <[email protected]>
karkhaz added a commit to aws/amazon-freertos that referenced this issue Mar 18, 2020
This commit attempts to preserve the functionality of the memory
checks in is_valid_IotResponseHandle until the issue in
diffblue/cbmc#5194 is sorted out. This
commit is needed because is_valid_IotRequestHandle and
is_valid_IotResponseHandle use the __CPROVER_r_ok and __CPROVER_w_ok
macros, and they are used inside an assume statement, triggering the
issue mentioned in that bug.
@danielsn
Copy link
Contributor Author

If the constraints are contradictory, how come the assert(0) in this example is reachable?

#include <assert.h>

int main()
{
    int* p;
    __CPROVER_assume(__CPROVER_r_ok(p,0));
    *p;
    assert(0);
}

@danielsn
Copy link
Contributor Author

The fact that this fails on 5.12 (cbmc-5.12-156-g8911c0adb) suggests that #5247 didn't fix all the issues.

@karkhaz
Copy link
Collaborator

karkhaz commented Apr 10, 2020

I believe that #5247 was never intended to fix this, it's supposed to fix negative uses inside an assert rather than positive uses inside assume.

But otherwise you're right: the fact that the assert(0) fails, but the possibly-null dereference of p is not caught, seems to refute the suggestion that r_ok inside assume causes contradictory assumptions. There must be more going on here.

@danielsn had tried this example because we were wondering whether the --insert-final-assert-false flag, introduced in #4985, would help us to catch such contradictory assumptions, but the above example shows that this will not work.

FTR The command to reproduce is

cbmc  --bounds-check --div-by-zero-check --float-overflow-check --nan-check --pointer-check --pointer-overflow-check --signed-overflow-check --undefined-shift-check --unsigned-overflow-check --unwind 1 --unwinding-assertions /tmp/test.c

as of 8911c0a.

@danpoe
Copy link
Contributor

danpoe commented Apr 14, 2020

We can distinguish between the semantics of the program, and cbmc's implementation of the semantics (which may be slightly overapproximate). According to the semantics, the above program would not be able to execute past the assume. However, due to cbmc's overapproximate behavior in that case, there are spurious executions in the encoding that can get past the assume, hence the assert(0) can fail. None of those executions however can violate the pointer checks implied by *p, thus this property does not fail.

I think there would be two possible ways of building confidence that assumptions aren't inconsistent:

  1. Instead of using assert(0), check that properties relevant to the assume that one would expect to fail can still fail, e.g. assert(__CPROVER_r_ok(p, 10)) for the program above

  2. Fixing the ways in which cbmc might overapproximate, enabling the use of assert(0) to detect inconsistent assumptions

@mww-aws
Copy link
Contributor

mww-aws commented Apr 14, 2020 via email

@karkhaz
Copy link
Collaborator

karkhaz commented Apr 16, 2020

We've tried to distil this issue into a series of precise test cases that capture what we think the problem is. We would expect CBMC to report failure on each of these programs when run with --bounds-check --pointer-check, but it reports no memory safety issues on any of them.

// Assume a buffer is 10 ints long, then assign to the 11th element.
int main(){
  int *p;
  __CPROVER_assume(__CPROVER_w_ok(p, 10 * sizeof(int)));
  p[10] = 0;
}
// sizeof int is larger than 1 byte, so having only 1 byte readable
// and dereferencing an int ought to fail. Note that we're assuming a
// readable length of 1, not (1 * sizeof(int)).
int main()
{
    int* p;
    __CPROVER_assume(__CPROVER_r_ok(p,1));
    *p;
}
// This represents a function stub that checks its input for validity. We
// often want to ensure that a data structure has enough space to read from or
// write to, so we assert that it does.
// We expect this to fail because we assert that a data structure has more space
// than it actually does.

void fun(int *p){
  __CPROVER_precondition(__CPROVER_w_ok(p, 11), "");
}

int main(){
  int *p;
  __CPROVER_assume(__CPROVER_w_ok(p, 10));
  fun(p);
}

As a variation on the above: if you write p[10] = 0 inside fun, CBMC actually does report a memory safety failure. Nevertheless, we still consider the lack of assertion failure on __CPROVER_precondition to be a problem for us.

// Very similar to the above program
int main(){
  int *p;
  __CPROVER_assume(__CPROVER_w_ok(p, 10));
  __CPROVER_assert(__CPROVER_w_ok(p, 11), "");
}

As a slightly different issue to all those examples above, we would expect the following program to have no memory safety issues, but CBMC reports that the assignment to p[9] is unsafe. This case is emphatically not the one that most worries us, however.

void fun(int *p){
  __CPROVER_precondition(__CPROVER_w_ok(p, 10 * sizeof(int)), "");
  p[9] = 0;
}

int main(){
  int *p;
  __CPROVER_assume(__CPROVER_w_ok(p, 10 * sizeof(int)));
  fun(p);
}

@danielsn
Copy link
Contributor Author

danielsn commented Apr 16, 2020

http://cprover.diffblue.com/cprover__builtin__headers_8h.html
Looking at this, I'm also concerned about:

__CPROVER_bool | __CPROVER_same_object (const void *, const void *)
__CPROVER_size_t | __CPROVER_POINTER_OBJECT (const void *)
__CPROVER_ssize_t | __CPROVER_POINTER_OFFSET (const void *)
__CPROVER_size_t | __CPROVER_OBJECT_SIZE (const void *)
__CPROVER_bool | __CPROVER_DYNAMIC_OBJECT (const void *)
__CPROVER_bool | __CPROVER_is_invalid_pointer (const void *)
__CPROVER_size_t | __CPROVER_buffer_size (const void *)
__CPROVER_bool | __CPROVER_r_ok (const void *, __CPROVER_size_t)
__CPROVER_bool | __CPROVER_w_ok (const void *, __CPROVER_size_t)

http://www.cprover.org/cprover-manual/api/

@danpoe
Copy link
Contributor

danpoe commented Apr 20, 2020

Thanks for those examples. To confirm our understanding, here's the next steps for us:

(1) We'll produce a testsuite to determine to what extent inconsistent assumptions involving memory primitives such as __CPROVER_same_object, etc. can be detected via the assert(0) check.

(2) We'll provide a cbmc option --invalid-pointers-in-assumptions-check to check that no invalid pointers are used in assumptions as this could be unintended.

(3) We'll provide a new primitive or library function __CPROVER_ensure_ok(p, n) (with n required to be >= 1) with the following semantics:

  • If p points to a memory block of size greater or equal to n: do nothing
  • If p is null or invalid: dynamically allocate n bytes and assign a pointer to the memory block to p
  • If p points to a memory block of size smaller than n: ?

For the last point there are essentially three options:

  • Terminate the path (that's what __CPROVER_assume would do)
  • Dynamically allocate n bytes and assign a pointer to the new memory block to p
  • Fail an assertion which checks that if p points to valid memory, the block is of at least size n

We'd tend towards the last option as it would be the most safe. Would you agree with that?

If possible, we'd like to avoid changing the existing semantics of __CPROVER_assume as others will rely on the current semantics.

@aws-arg-padstone-ci-awslabs-s2n

I agree that failing an assertion would be the safest thing in that situation.

@mww-aws
Copy link
Contributor

mww-aws commented Apr 20, 2020

The steps look appropriate and should help us in the future. These changes are very high priority for us (we have to report to our management about it), is it possible to have (1) and (2) implemented in one week's time? I realize that this is abrupt, but it would be very helpful to us. Having at least (1) would allow us to easily double check all of our proofs.

@danpoe
Copy link
Contributor

danpoe commented Apr 21, 2020

Yes, it should be possible to provide (1) and (2) within a week.

@danpoe
Copy link
Contributor

danpoe commented Apr 27, 2020

We've now created the testsuites of point (1). We checked the primitives listed by @danielsn above (except __CPROVER_is_invalid_pointer() and __CPROVER_buffer_size, these two are specific to cbmc-internal analyses). In summary, for assumptions involving __CPROVER_r_ok(), __CPROVER_w_ok(), __CPROVER_DYNAMIC_OBJECT, and __CPROVER_OBJECT_SIZE, the assumptions can be inconsistent according to the intended semantics of the primitives, and this may not be detectable via the assert(0) check due to overapproximation.

For (2), we've added a new option --pointer-primitive-check to cbmc. It verifies that all pointers that appear in the pointer primitives (again, those listed by @danielsn above except __CPROVER_is_invalid_pointer and __CPROVER_buffer_size) are either null or point to valid memory. If all those checks succeed, then inconsistent assumptions involving the primitives can be detected via the assert(0) check.

@danielsn
Copy link
Contributor Author

@danpoe which are the PRs that add those?

@danpoe
Copy link
Contributor

danpoe commented Apr 27, 2020

PR #5318 is for --pointer-primitive-check (merged), and PRs #5321 and #5324 are for the tests (not yet merged).

@danielsn
Copy link
Contributor Author

@danpoe Thanks. I've added comments on those PRs.

One thing that would really help is a document listing in one place the expected semantics of each primitive. Does such a document exist / would it be possible to create one?

@danpoe
Copy link
Contributor

danpoe commented Jun 29, 2020

The behavior of the primitives (including preconditions) has been documented at http://cprover.diffblue.com/memory-primitives.html. In addition, there is now an option --pointer-primitive-check in cbmc to check the preconditions of the primitives.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
aws Bugs or features of importance to AWS CBMC users
Projects
None yet
Development

No branches or pull requests

7 participants