Description
CBMC version: 5.67.0
Operating system: N/A
How to reproduce
#include <stdbool.h>
#include <stdlib.h>
#define SUCCESS 1
#define FAILURE 0
#define IMPLIES(a, b) (!(a) || (b))
struct obj_st;
/** A function table. */
typedef struct ftbl_st {
int (*set_value)(struct obj_st *);
} ftbl_st;
/** An object. */
typedef struct obj_st {
int a;
ftbl_st *ftbl;
} obj_st;
static bool set_once = 0;
static bool __is_in_mode_0 = 0;
bool is_in_mode_0() {
if (!set_once) {
set_once = 1;
__is_in_mode_0 = nondet_int() >= 0 ? true : false;
}
return __is_in_mode_0;
}
static int set_value_0(obj_st *obj) {
if (obj == NULL)
return FAILURE;
obj->a = 0;
return SUCCESS;
}
static int set_value_1(obj_st *obj) {
if (obj == NULL)
return FAILURE;
obj->a = 1;
return SUCCESS;
}
static const ftbl_st ftbl_0 = { .set_value = set_value_0 };
static const ftbl_st ftbl_1 = { .set_value = set_value_1 };
int set_value(obj_st *obj)
// obj is either null or some fresh object
__CPROVER_requires(IMPLIES(obj != NULL, __CPROVER_is_fresh(obj, sizeof(*obj))))
// the function table of the object is only one of two possible tables, which depends on the current mode
__CPROVER_requires(IMPLIES(obj != NULL && is_in_mode_0(), obj->ftbl == &ftbl_0))
__CPROVER_requires(IMPLIES(obj != NULL && !is_in_mode_0(), obj->ftbl == &ftbl_1))
// if the function succeeds, then obj is not null and updated according to the mode
__CPROVER_ensures(IMPLIES(__CPROVER_return_value == SUCCESS && is_in_mode_0(), obj != NULL && obj->a == 0))
__CPROVER_ensures(IMPLIES(__CPROVER_return_value == SUCCESS && !is_in_mode_0(), obj != NULL && obj->a == 1))
// for convenience
__CPROVER_assigns(obj->ftbl)
{
if (obj == NULL)
return FAILURE;
// adding this statement makes everything work as expected
//obj->ftbl = is_in_mode_0()? &ftbl_0 : &ftbl_1;
// this assertion is proved, showing that the contract preconditions are enforced
__CPROVER_assert(obj->ftbl == &ftbl_0 || obj->ftbl == &ftbl_1, "two_tables");
// we copy the obj fields in local vars for diagnosis
obj_st dummy0 = *obj;
ftbl_st dummy1 = *(dummy0.ftbl);
int (*dummy2)(obj_st *) = dummy1.set_value;
int (*dummy3)(obj_st *) = dummy2;
// this assertion fails, but it should not since the "two_tables" assertion holds
// and ftbl_0 and ftbl_1 have non-null function pointers.
//__CPROVER_assert(obj->ftbl->set_value != NULL, "function pointer not null");
__CPROVER_assert(dummy3 != NULL, "function_pointer_is_not_null");
int res;
if (dummy3 == set_value_0)
res = set_value_0(obj);
else if (dummy3 == set_value_1)
res = set_value_1(obj);
else
__CPROVER_assert(false, "unknown_function_pointer");
return res;
}
all:
# export static symbols
goto-cc --export-file-local-symbols example.c -o example.c1.goto
# remove function pointers calls
# goto-instrument --restrict-function-pointer set_value.function_pointer_call.1/__CPROVER_file_local_example_c_set_to_0,__CPROVER_file_local_example_c_set_to_1 example.c1.goto example.c2.goto
cp example.c1.goto example.c2.goto
# compile the set_value function
goto-cc --function set_value example.c2.goto -o example.c3.goto
# build contract-checking program
goto-instrument --enforce-contract set_value example.c3.goto example.c4.goto
# drop unused functions
goto-instrument --drop-unused-functions example.c4.goto example.c5.goto
#cp example.c4.goto example.c5.goto
# slice initialisation
goto-instrument --slice-global-inits example.c5.goto example.c6.goto
#cp example.c5.goto example.c6.goto
# show functions
goto-instrument --show-goto-functions example.c6.goto > example.c6.txt
# compute coverage
cbmc --unwind 1 --flush --object-bits 8 --malloc-may-fail --malloc-fail-null --cover location example.c6.goto >coverage.txt 2>coverage-err.log
# compute properties
cbmc --unwind 1 --flush --object-bits 8 --pointer-primitive-check --malloc-may-fail --malloc-fail-null --bounds-check --conversion-check --div-by-zero-check --float-overflow-check --nan-check --pointer-check --pointer-overflow-check --pointer-primitive-check --signed-overflow-check --undefined-shift-check --unsigned-overflow-check --unwinding-assertions --show-properties $(XML_UI) example.c6.goto >property.txt 2>property-err.log
# dump program POs
cbmc --program-only --unwind 1 --flush --object-bits 8 --pointer-primitive-check --malloc-may-fail --malloc-fail-null --bounds-check --conversion-check --div-by-zero-check --float-overflow-check --nan-check --pointer-check --pointer-overflow-check --pointer-primitive-check --signed-overflow-check --undefined-shift-check --unsigned-overflow-check --unwinding-assertions --trace $(XML_UI) example.c6.goto >result-program-only.txt 2>result-program-only-err.log
# analyse program POs
cbmc --unwind 1 --flush --object-bits 8 --pointer-primitive-check --malloc-may-fail --malloc-fail-null --bounds-check --conversion-check --div-by-zero-check --float-overflow-check --nan-check --pointer-check --pointer-overflow-check --pointer-primitive-check --signed-overflow-check --undefined-shift-check --unsigned-overflow-check --unwinding-assertions --trace $(XML_UI) example.c6.goto >result.txt 2>result-err.log
clean:
rm -f *.goto *.txt *.log *~
.PHONY: all clean
Run make all
.
Assertions are falsified when they shouldn't.
The issue
Consider the function below:
int set_value(obj_st *obj)
// obj is either null or some fresh object
__CPROVER_requires(IMPLIES(obj != NULL, __CPROVER_is_fresh(obj, sizeof(*obj))))
// the function table of the object is only one of two possible tables, which depends on the current mode
__CPROVER_requires(IMPLIES(obj != NULL && is_in_mode_0(), obj->ftbl == &ftbl_0))
__CPROVER_requires(IMPLIES(obj != NULL && !is_in_mode_0(), obj->ftbl == &ftbl_1))
// if the function succeeds, then obj is not null and updated according to the mode
__CPROVER_ensures(IMPLIES(__CPROVER_return_value == SUCCESS && is_in_mode_0(), obj != NULL && obj->a == 0))
__CPROVER_ensures(IMPLIES(__CPROVER_return_value == SUCCESS && !is_in_mode_0(), obj != NULL && obj->a == 1))
// for convenience
__CPROVER_assigns(obj->ftbl)
{
if (obj == NULL)
return FAILURE;
// adding this statement makes everything work as expected
//obj->ftbl = is_in_mode_0()? &ftbl_0 : &ftbl_1;
// this assertion is proved, showing that the contract preconditions are enforced
__CPROVER_assert(obj->ftbl == &ftbl_0 || obj->ftbl == &ftbl_1, "two_tables");
// we copy the obj fields in local vars to have CBMC print them in the trace for diagnosis
obj_st dummy0 = *obj;
ftbl_st dummy1 = *(dummy0.ftbl);
int (*dummy2)(obj_st *) = dummy1.set_value;
int (*dummy3)(obj_st *) = dummy2;
// this assertion fails, but it should not since the "two_tables" assertion holds
// and ftbl_0 and ftbl_1 have non-null function pointers.
//__CPROVER_assert(obj->ftbl->set_value != NULL, "function pointer not null");
__CPROVER_assert(dummy3 != NULL, "function_pointer_is_not_null");
int res;
if (dummy3 == set_value_0)
res = set_value_0(obj);
else if (dummy3 == set_value_1)
res = set_value_1(obj);
else
__CPROVER_assert(false, "unknown_function_pointer");
return res;
}
The contract says that the obj->ftbl
can point to ftbl_0
or ftbl_1
, two static structs that contain a field .set_value
which is a function pointer to function set_value_0
or set_value_1
respectively. The contract declares the conditions under which ftbl_0
or ftlb_1
gets used, and ensures the post conditions of whichever one is used. The contract and implementation are correct wrt each other.
Yet the verification of this contract fails.
for the postcondition.1
assertion: The function table in dummy0 is set to ftbl_1, but extracting the function from the function table yields set_value_0 in dummy_1, should be set_value_1. So it really seems like any value can be dreamed up from this function pointer.
State 193 file example.c function set_value line 103 thread 0
----------------------------------------------------
dummy0.ftbl=&ftbl_1 (00001001 00000000 00000000 00000000 00000000 00000000 00000000 00000000)
State 194 file example.c function set_value line 104 thread 0
----------------------------------------------------
dummy1={ .set_value=dummy1!0@1#1..set_value } ({ ? })
State 203 file example.c function set_value line 104 thread 0
----------------------------------------------------
dummy1={ .set_value=set_value_0 } ({ 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000 })
For the set_value.assertion.2
assertion: the null function pointer back-propagates through the dummy assignments:
State 203 file example.c function set_value line 104 thread 0
----------------------------------------------------
dummy1={ .set_value=((signed int (*)(struct obj_st *))NULL) } ({ 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 })
State 204 file example.c function set_value line 104 thread 0
----------------------------------------------------
dummy1.set_value=((signed int (*)(struct obj_st *))NULL) (00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000)
State 205 file example.c function set_value line 105 thread 0
----------------------------------------------------
dummy2=((signed int (*)(struct obj_st *))NULL) (00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000)
State 208 file example.c function set_value line 105 thread 0
----------------------------------------------------
dummy2=((signed int (*)(struct obj_st *))NULL) (00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000)
State 209 file example.c function set_value line 106 thread 0
----------------------------------------------------
dummy3=((signed int (*)(struct obj_st *))NULL) (00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000)
State 212 file example.c function set_value line 106 thread 0
----------------------------------------------------
dummy3=((signed int (*)(struct obj_st *))NULL) (00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000)
for the set_value.assertion.3
, the dummy3 function pointer is equal to
set_value_0` + a non-zero offset ?
(Are function pointers encoded as object pointers in CBMC ?)
State 205 file example.c function set_value line 105 thread 0
----------------------------------------------------
dummy2=((signed int (*)(struct obj_st *))NULL) (00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000)
State 208 file example.c function set_value line 105 thread 0
----------------------------------------------------
dummy2=set_value_0 + 262144l (00000010 00000000 00000000 00000000 00000000 00000100 00000000 00000000)
State 209 file example.c function set_value line 106 thread 0
----------------------------------------------------
dummy3=((signed int (*)(struct obj_st *))NULL) (00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000)
State 212 file example.c function set_value line 106 thread 0
----------------------------------------------------
dummy3=set_value_0 + 262144l (00000010 00000000 00000000 00000000 00000000 00000100 00000000 00000000)
State 214 file example.c function set_value line 119 thread 0
----------------------------------------------------
res=1 (00000000 00000000 00000000 00000001)
Violated property:
file example.c function set_value line 126 thread 0
unknown_function_pointer
0 != 0
Here’s what is odd (in the program dump obtained using cbmc --program-only ...
):
// 323 file example.c line 104 function set_value
(276) dummy1!0@1#1 == { .set_value=dummy0!0@1#2..ftbl == &ftbl_0 ? set_value_0 : invalid_object!0#0..set_value }
guard: !\guard#8
when dummy1
gets assigned from dummy0
it seems like cbmc
assumes rather arbitrarily that dummy0.ftbl
is either ftbl_0
or some invalid object and can be nothing else.
However, what the contract tries to say is that dummy0
is free function pointer required to be equal to one of two possible addresses &ftbl_0
or &ftbl_1
(see the contract's __CPROVER_requires
clauses of the contract).
So we see that this C code
obj_st dummy0 = *obj;
yields this goto code :
(267) dummy0!0@1#2..ftbl == dynamic_object1#5..ftbl
and this realtively similar C-code :
ftbl_st dummy1 = *(dummy0.ftbl);
yields this very different goto code:
(276) dummy1!0@1#1 == { .set_value=dummy0!0@1#2..ftbl == &ftbl_0 ? set_value_0 : invalid_object!0#0..set_value }
I would have expected something like
(276) dummy1!0@1#1 == dummy0!0@1#2..ftbl
It may be the encoding of the deref operation on *(dummy.ftbl)
(since dummy0.ftbl
is a pointer, the deref could fail)
so this is modelled like this, but it still does not explain why CBMC assumes that dummy0!0@1#2..ftbl
can only be equal to &ftbl_0
(in reality it can also point to &ftbl_1
).
Next investigation steps
- Check what is the rule used by CMBC to generate this goto code and the rationale behind it.
- Check if CBMC really allows offsets on function pointers.