Skip to content

Find a proper way to specify pointers to static objects in contract preconditions #7195

Open
@feliperodri

Description

@feliperodri

CBMC version: 5.37.0
Operating system: N/A

How to reproduce

#include <stdlib.h>
#include <stdbool.h>

typedef struct b_st {
    int v;
} b_st;

static const b_st b1 = { .v = 1 };
static const b_st b2 = { .v = 2 };

typedef struct a_st {
    int v;
    struct b_st* b;
} a_st;

a_st * new_a() {
    a_st *a = malloc(sizeof(*a));
    a->v = 0;
    a->b = nondet_int()%2 ? &b1 : &b2;
    return a;
}

bool b_is_set(a_st *a) {
    if (!__CPROVER_w_ok(a)) return false;
    a->b = nondet_int()%2 ? &b1 : &b2;
    return true;
}

int get_b(a_st *a)
__CPROVER_requires(__CPROVER_is_fresh(a, sizeof(*a)))
// __CPROVER_requires(b_is_set(a)) // PROBLEM 3
// __CPROVER_requires(__CPROVER_is_fresh(a->b, sizeof(*(a->b)))) // PROBLEM 1 
__CPROVER_requires(a->b == &b1 || a->b == &b2)
// __CPROVER_requires(__CPROVER_is_fresh(a->b, sizeof(*(a->b)))) // PROBLEM 2 
__CPROVER_ensures(__CPROVER_return_value == 1 || __CPROVER_return_value == 2)
{
    // a->b = nondet_int()%2 ? &b1 : &b2; // PROBLEM 4
    return a->b->v;
}
all:
	goto-cc --export-file-local-symbols example.c -o example.c1.goto	
	goto-cc --function get_b example.c1.goto -o example.c2.goto	
	goto-instrument --enforce-contract get_b example.c2.goto example.c3.goto
	goto-instrument --drop-unused-functions example.c3.goto example.c4.goto
	goto-instrument --slice-global-inits example.c4.goto example.c5.goto
	goto-instrument --show-goto-functions example.c5.goto > example.c5.txt
	cbmc --unwind 1  --flush --object-bits 8 --malloc-may-fail --malloc-fail-null --cover location example.c5.goto >coverage.txt 2>coverage-err.log
	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.c5.goto >property.txt 2>property-err.log
	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.c5.goto >result-program-only.txt 2>result-program-only-err.log
	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.c5.goto >result.txt 2>result-err.log

clean: 
	rm -f *.goto *.txt *.log *~

.PHONY: all clean

Run make all.

The issue

Using the current approach relying on __CPROVER_is_fresh(p,size) it is impossible to describe objects which are partly malloc'd and partly pointing to static objects.

In this example struct a has a field b that is meant to point to one of two static objects b1 or b2 (check the new_a() function).

In the get_b function contract, we want to express that a is fresh, and that a->b points to either static const b1 or static const b2.

problem 0

typedef struct b_st {
    int v;
} b_st;

static const b_st b1 = { .v = 1 };
static const b_st b2 = { .v = 2 };

typedef struct a_st {
    int v;
    struct b_st* b;
} a_st;

a_st * new_a() {
    a_st *a = malloc(sizeof(*a));
    a->v = 0;
    a->b = nondet_int()%2 ? &b1 : &b2;
    return a;
}

int get_b(a_st *a)
__CPROVER_requires(__CPROVER_is_fresh(a, sizeof(*a)))
__CPROVER_requires(a->b == &b1 || a->b == &b2)
__CPROVER_ensures(__CPROVER_return_value == 1 || __CPROVER_return_value == 2)
{
    return a->b->v;
}

Analysing this contract we get an error trace:

tail restult.txt
[pointer_primitives.1] file example.c line 80 pointer invalid in POINTER_OBJECT(tmp_cc$0->b): FAILURE
[pointer_primitives.4] file example.c line 80 pointer outside object bounds in POINTER_OBJECT(tmp_cc$0->b): FAILURE
[pointer_primitives.8] file example.c line 80 pointer outside object bounds in POINTER_OBJECT(tmp_cc$0->b): FAILURE
[pointer_primitives.12] file example.c line 80 pointer outside object bounds in POINTER_OBJECT(tmp_cc$0->b): FAILURE
[pointer_primitives.16] file example.c line 80 pointer outside object bounds in POINTER_OFFSET(tmp_cc$0->b): FAILURE
[pointer_primitives.20] file example.c line 80 pointer outside object bounds in POINTER_OFFSET(tmp_cc$0->b): FAILURE
[pointer_primitives.24] file example.c line 80 pointer outside object bounds in OBJECT_SIZE(tmp_cc$0->b): FAILURE
[postcondition.1] file example.c line 81 Check ensures clause: FAILURE

problem 1

Adding a requirement that a->b is some valid object using is_fresh

int get_b(a_st *a)
__CPROVER_requires(__CPROVER_is_fresh(a, sizeof(*a)))
__CPROVER_requires(__CPROVER_is_fresh(a->b, sizeof(*(a->b))))
__CPROVER_requires(a->b == &b1 || a->b == &b2)
__CPROVER_ensures(__CPROVER_return_value == 1 || __CPROVER_return_value == 2)
{
    return a->b->v;
}

We get a vacuous instance: proving all assertions with zero code coverage

❯ tail result.txt
[get_b.pointer_dereference.6] line 84 dereference failure: invalid integer address in a->b: SUCCESS
[get_b.pointer_dereference.7] line 84 dereference failure: pointer NULL in a->b->v: SUCCESS
[get_b.pointer_dereference.8] line 84 dereference failure: pointer invalid in a->b->v: SUCCESS
[get_b.pointer_dereference.9] line 84 dereference failure: deallocated dynamic object in a->b->v: SUCCESS
[get_b.pointer_dereference.10] line 84 dereference failure: dead object in a->b->v: SUCCESS
[get_b.pointer_dereference.11] line 84 dereference failure: pointer outside object bounds in a->b->v: SUCCESS
[get_b.pointer_dereference.12] line 84 dereference failure: invalid integer address in a->b->v: SUCCESS

** 0 of 62 failed (1 iterations)
VERIFICATION SUCCESSFUL
❯ tail coverage.txt
[__CPROVER_contracts_original_get_b.coverage.1] file example.c line 84 function __CPROVER_contracts_original_get_b block 1 (lines example.c:get_b:84,85): FAILED
[get_b.coverage.13] file example.c line 82 function get_b block 13 (lines example.c::82; instrumented for code contracts::0): FAILED
[get_b.coverage.12] file example.c line 82 function get_b block 12 (lines example.c::82): FAILED
[get_b.coverage.6] file example.c line 81 function get_b block 6 (lines example.c::81): FAILED

problem 3

Adding the is_fresh assertion after the other assertion does not result in a vacuous instance but allows to falsify the post condition with a spurious trace.

int get_b(a_st *a)
__CPROVER_requires(__CPROVER_is_fresh(a, sizeof(*a)))
__CPROVER_requires(a->b == &b1 || a->b == &b2)
__CPROVER_requires(__CPROVER_is_fresh(a->b, sizeof(*(a->b))))
__CPROVER_ensures(__CPROVER_return_value == 1 || __CPROVER_return_value == 2)
{
    return a->b->v;
}
❯ tail result.txt

Violated property:
  file example.c function get_b line 82 thread 0
  Check ensures clause
  tmp_cc == 1 || tmp_cc == 2



** 1 of 62 failed (2 iterations)
VERIFICATION FAILED
❯ tail coverage.txt
[get_b.coverage.13] file example.c line 82 function get_b block 13 (lines example.c::82; instrumented for code contracts::0): SATISFIED
[get_b.coverage.12] file example.c line 82 function get_b block 12 (lines example.c::82): SATISFIED
[get_b.coverage.11] file example.c line 80 function get_b block 11 (lines example.c::80): SATISFIED
[get_b.coverage.9] file example.c line 81 function get_b block 9 (lines example.c::81): SATISFIED
[get_b.coverage.8] file example.c line 81 function get_b block 8 (lines example.c::81): SATISFIED
[get_b.coverage.7] file example.c line 81 function get_b block 7 (lines example.c::81): SATISFIED
[get_b.coverage.6] file example.c line 81 function get_b block 6 (lines example.c::81): SATISFIED
[get_b.coverage.5] file example.c line 80 function get_b block 5 (lines example.c::80): SATISFIED
[get_b.coverage.3] file example.c line 80 function get_b block 3 (lines example.c::80): SATISFIED
[get_b.coverage.2] file example.c line 79 function get_b block 2 (lines example.c::79,80): SATISFIED
[get_b.coverage.1] file example.c line 82 function get_b block 1 (lines example.c::79,82): SATISFIED
[get_b.coverage.4] file example.c line 80 function get_b block 4 (lines example.c::80): SATISFIED
[get_b.coverage.10] file example.c line 80 function get_b block 10 (lines example.c::80): SATISFIED
[__CPROVER_contracts_original_get_b.coverage.1] file example.c line 84 function __CPROVER_contracts_original_get_b block 1 (lines example.c:get_b:84,85): SATISFIED

workaround 1

Use a side-effecting predicate that assigns a->b in the preconditions which is not ideal.

bool b_is_set(a_st *a) {
    if (!__CPROVER_w_ok(a)) return false;
    a->b = nondet_int()%2 ? &b1 : &b2;
    return true;
}

int get_b(a_st *a)
__CPROVER_requires(__CPROVER_is_fresh(a, sizeof(*a)))
__CPROVER_requires(b_is_set(a))
__CPROVER_ensures(__CPROVER_return_value == 1 || __CPROVER_return_value == 2)
{
    return a->b->v;
}

workaround 2

assign a->b = nondet_int()%2 ? &b1 : &b2; in the function body. Works but modifies the code.

Metadata

Metadata

Labels

Code ContractsFunction and loop contractsawsBugs or features of importance to AWS CBMC usersfeature request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions