Skip to content

CONTRACTS: Add __CPROVER_frees clauses in the front end #7091

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions doc/cprover-manual/contracts-frees.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
[CPROVER Manual TOC](../../)

# Frees Clauses

A _frees clause_ allows the user to specify a set of pointers that may be freed
by a function or by the function it calls, transitively.
A function contract may have zero or more frees clauses.
When no clause is provided the empty set is used as default.
Contracts can also have an empty frees clause.
When more than one frees clause is given, the sets of pointers they contain are
unioned together to yield a single set of pointers.

## Syntax

The clause has the following syntax (square brackets denote optional expressions
`[` `]` ):
```c
__CPROVER_frees([targets])
```

Where `targets` has the following syntax:
```
targets ::= cond-target-group (';' cond-target-group)* ';'?
cond-target-group ::= (condition ':')? target (',' target)*
target ::= lvalue-expr
| __CPROVER_freeable(lvalue-expr)
```

A frees clause target must be either:
- an lvalue expression with a pointer type,
- a call to the built-in function `__CPROVER_freeable`
- a call to a user-defined side effect free and deterministic function returning
the type `void` (itself containing direct or indirect calls to
`__CPROVER_freeable` or to functions that call `__CPROVER_freeable`);

### Example

```c
int foo(char *arr1, char *arr2, size_t size)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these examples included as regression tests? If so, perhaps there is some way to link to them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no these are just embedded in the doc.

__CPROVER_frees(
// `arr1` is freeable only if the condition `size > 0 && arr1` holds
size > 0 && arr1: arr1;

// `arr2` is always freeable
arr2;
)
{
if(size > 0 && arr1)
free(arr1);
free(arr2);
return 0;
}
```

## Semantics

The set of pointers specified by the frees clause of the contract is interpreted
at the function call-site where the contract is being checked or used to abstract
a function call.

### For contract checking

When checking a contract against a function, each pointer that the
function attempts to free gets checked for membership in the set of
pointers specified by the _frees clause_.

### For replacement of function calls by contracts

When replacing a function call by a contract, each pointer of the
_frees clause_ is non-deterministically freed after the function call.

## Specifying parametric sets of freeable pointers using C functions
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any restrictions on what control flow is permitted in such functions? For example, are recursion or loops without constant bound permitted?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this first version recursion is prohibited, the function body is going to be inlined, and any remaining loops must be unwound to completion before the analysis. Adding this to the documentation.


Users can define parametric sets of freeable pointers by writing functions that
return the built-in type void and call the built-in function
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return the built-in type void and call the built-in function
return the built-in type `void` and call the built-in function

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

`__CPROVER_freeable` (directly or indirectly through some other user-defined
function). The functions must be side-effect free and deterministic,
as well as loop-free and recursion-free.

```c
void my_freeable_set(char **arr, size_t size)
{
// The first 3 pointers are freeable
// if the array is at least of size 3.
if (arr && size > 3) {
__CPROVER_freeable(arr[0]);
__CPROVER_freeable(arr[1]);
__CPROVER_freeable(arr[2]);
}
}
```

The built-in function:
```c
void __CPROVER_freeable(void *ptr);
```
adds the given pointer to the freeable set described by the enclosing function.

Calls to such functions can be used as targets in `__CPROVER_frees` clauses:
```c
void my_function(char **arr, size_t size)
__CPROVER_frees(my_freeable_set(arr, size))
{
...
}
```

## Frees clause related predicates

The predicate:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this line.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The predicate:

```c
__CPROVER_bool __CPROVER_is_freeable(void *ptr);
```
can only be used in pre and post conditions, in contract checking or replacement
modes. It returns `true` if and only if the pointer satisfies the preconditions
of the `free` function from `stdlib.h`
([see here](https://github.com/diffblue/cbmc/blob/cf00a53bbcc388748be9668f393276f6d84b1a60/src/ansi-c/library/stdlib.c#L269)),
that is if and only if the pointer points to a valid dynamically allocated object and has offset zero.

The predicate:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this line.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The predicate:

```c
__CPROVER_bool __CPROVER_was_freed(void *ptr);
```
can only be used in post conditions and returns `true` if and only if the
pointer was freed during the execution of the function under analysis.
Comment on lines +124 to +125
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this also work when the pointer is re-allocated after having been freed? As in:

free(p);
p = malloc(1);

with the call to malloc returning the same adddress that p had before?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change the name of the predicate to __CPROVER_was_freed.

In a situation like this both __CPROVER_was_freed(p) and __CPROVER_r_ok(p, 1) will hold in post.

The instrumentation keeps track of deallocation events by recording ids of deallocated objects in a map, that gets passed to the predicate as a ghost parameter for evaluation.

if we wanted instead to assert that a pointer does not point into a live object as a post we would need to track the lifetime and current size of all objects in the program separately (without nondeterminism as is currently done).

45 changes: 45 additions & 0 deletions regression/contracts/frees-clause-and-predicates-fail/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include <stdlib.h>

// A function defining a conditionally freeable target
void foo_frees(char *arr, const size_t size, const size_t new_size)
{
__CPROVER_freeable(arr);
}

char *foo(char *arr, const size_t size, const size_t new_size)
// clang-format off
// error was_freed cannot be used in preconditions
__CPROVER_requires(!__CPROVER_was_freed(arr))
__CPROVER_requires(__CPROVER_is_freeable(arr))
__CPROVER_assigns(__CPROVER_object_whole(arr))
__CPROVER_frees(foo_frees(arr, size, new_size))
__CPROVER_ensures(
(arr && new_size > size) ==>
__CPROVER_is_fresh(__CPROVER_return_value, new_size))
__CPROVER_ensures(
(arr && new_size > size) ==>
__CPROVER_was_freed(__CPROVER_old(arr)))
__CPROVER_ensures(
!(arr && new_size > size) ==>
__CPROVER_return_value == __CPROVER_old(arr))
// clang-format on
{
if(arr && new_size > size)
{
free(arr);
return malloc(new_size);
}
else
{
return arr;
}
}

int main()
{
size_t size;
size_t new_size;
char *arr = malloc(size);
arr = foo(arr, size, new_size);
return 0;
}
10 changes: 10 additions & 0 deletions regression/contracts/frees-clause-and-predicates-fail/test.desc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CORE
main.c
--enforce-contract foo
^main.c.* error: __CPROVER_was_freed is not allowed in preconditions.$
^CONVERSION ERROR$
^EXIT=(1|64)$
^SIGNAL=0$
--
--
This test checks that the front end rejects __CPROVER_was_freed in preconditions.
44 changes: 44 additions & 0 deletions regression/contracts/frees-clause-and-predicates-fail2/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include <stdlib.h>

// A function defining a conditionally freeable target
int foo_frees(char *arr, const size_t size, const size_t new_size)
{
__CPROVER_freeable(arr);
return 0;
}

char *foo(char *arr, const size_t size, const size_t new_size)
// clang-format off
__CPROVER_requires(__CPROVER_is_freeable(arr))
__CPROVER_assigns(__CPROVER_object_whole(arr))
__CPROVER_frees(foo_frees(arr, size, new_size))
__CPROVER_ensures(
(arr && new_size > size) ==>
__CPROVER_is_fresh(__CPROVER_return_value, new_size))
__CPROVER_ensures(
(arr && new_size > size) ==>
__CPROVER_was_freed(__CPROVER_old(arr)))
__CPROVER_ensures(
!(arr && new_size > size) ==>
__CPROVER_return_value == __CPROVER_old(arr))
// clang-format on
{
if(arr && new_size > size)
{
free(arr);
return malloc(new_size);
}
else
{
return arr;
}
}

int main()
{
size_t size;
size_t new_size;
char *arr = malloc(size);
arr = foo(arr, size, new_size);
return 0;
}
11 changes: 11 additions & 0 deletions regression/contracts/frees-clause-and-predicates-fail2/test.desc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CORE
main.c
--enforce-contract foo
^main.c.* error: expecting void return type for function 'foo_frees' called in frees clause$
^CONVERSION ERROR$
^EXIT=(1|64)$
^SIGNAL=0$
--
--
This test checks that the front-end rejects non-void-typed
function calls in frees clauses.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include <stdlib.h>

void foo(char *arr) __CPROVER_requires(__CPROVER_is_freeable(arr, 1))
{
}

int main()
{
size_t size;
char arr[size];
foo(arr);
return 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CORE
main.c
--enforce-contract foo
^main.c.*error: wrong number of function arguments: expected 1, but got 2$
^CONVERSION ERROR$
^EXIT=(1|64)$
^SIGNAL=0$
--
--
This test checks bad uses of __CPROVER_is_freeable are rejected.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#include <stdlib.h>

void foo(char *arr) __CPROVER_requires(__CPROVER_is_freeable(arr))
__CPROVER_ensures(__CPROVER_was_freed(__CPROVER_old(arr), 1))
{
free(arr);
}

int main()
{
size_t size;
char arr[size];
foo(arr);
return 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CORE
main.c
--enforce-contract foo
^main.c.*error: wrong number of function arguments: expected 1, but got 2$
^CONVERSION ERROR$
^EXIT=(1|64)$
^SIGNAL=0$
--
--
This test checks bad uses of __CPROVER_was_freed are rejected.
44 changes: 44 additions & 0 deletions regression/contracts/frees-clause-and-predicates/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include <stdlib.h>

// A function defining freeable pointers
void foo_frees(char *arr, const size_t size, const size_t new_size)
{
__CPROVER_freeable(arr);
}

// Function that resizes the array
char *foo(char *arr, const size_t size, const size_t new_size)
// clang-format off
__CPROVER_requires(__CPROVER_is_freeable(arr))
__CPROVER_assigns(__CPROVER_object_whole(arr))
__CPROVER_frees(foo_frees(arr, size, new_size))
__CPROVER_ensures(
(arr && new_size > size) ==>
__CPROVER_is_fresh(__CPROVER_return_value, new_size))
__CPROVER_ensures(
(arr && new_size > size) ==>
__CPROVER_was_freed(__CPROVER_old(arr)))
__CPROVER_ensures(
!(arr && new_size > size) ==>
__CPROVER_return_value == __CPROVER_old(arr))
// clang-format on
{
if(arr && new_size > size)
{
free(arr);
return malloc(new_size);
}
else
{
return arr;
}
}

int main()
{
size_t size;
size_t new_size;
char *arr = malloc(size);
arr = foo(arr, size, new_size);
return 0;
}
17 changes: 17 additions & 0 deletions regression/contracts/frees-clause-and-predicates/test.desc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CORE
main.c
--enforce-contract foo
^\[foo.postcondition.\d+\] line \d+ Check ensures clause: FAILURE$
^VERIFICATION FAILED$
^EXIT=10$
^SIGNAL=0$
--
--
This test checks that the front end parses and typechecks correct uses of
- void function calls as frees clause targets
- the predicate __CPROVER_freeable
- the predicate __CPROVER_is_freeable
- the predicate __CPROVER_was_freed

The post condition of the contract is expected to fail because the predicates
have no interpretation in the back-end yet.
11 changes: 11 additions & 0 deletions src/ansi-c/ansi_c_convert_type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,14 @@ void ansi_c_convert_typet::read_rec(const typet &type)
for(const exprt &target : to_unary_expr(as_expr).op().operands())
assigns.push_back(target);
}
else if(type.id() == ID_C_spec_frees)
{
const exprt &as_expr =
static_cast<const exprt &>(static_cast<const irept &>(type));

for(const exprt &target : to_unary_expr(as_expr).op().operands())
frees.push_back(target);
}
else if(type.id() == ID_C_spec_ensures)
{
const exprt &as_expr =
Expand Down Expand Up @@ -341,6 +349,9 @@ void ansi_c_convert_typet::write(typet &type)
if(!assigns.empty())
to_code_with_contract_type(type).assigns() = std::move(assigns);

if(!frees.empty())
to_code_with_contract_type(type).frees() = std::move(frees);

if(!ensures.empty())
to_code_with_contract_type(type).ensures() = std::move(ensures);

Expand Down
2 changes: 1 addition & 1 deletion src/ansi-c/ansi_c_convert_type.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class ansi_c_convert_typet:public messaget
bool constructor, destructor;

// contracts
exprt::operandst assigns, ensures, requires, ensures_contract,
exprt::operandst assigns, frees, ensures, requires, ensures_contract,
requires_contract;

// storage spec
Expand Down
Loading