-
Notifications
You must be signed in to change notification settings - Fork 274
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
CONTRACTS: Add __CPROVER_frees clauses in the front end #7091
Conversation
aa4af69
to
38f3ce1
Compare
Codecov ReportBase: 77.89% // Head: 77.88% // Decreases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## develop #7091 +/- ##
===========================================
- Coverage 77.89% 77.88% -0.01%
===========================================
Files 1616 1616
Lines 186701 186828 +127
===========================================
+ Hits 145433 145516 +83
- Misses 41268 41312 +44
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
de218d2
to
20ee9e8
Compare
|
||
### Syntax | ||
The set of locations described by the contract is the union of the locations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is a "location" here a "memory location?" Also, is "contract" well defined here? There is no prior mention of this in this particular document. Also, now that I re-read this paragraph: in what way does a contract describe a set of (memory) locations? Why does that even matter?
### Syntax | ||
The set of locations described by the contract is the union of the locations | ||
specified by each of its assigns clauses, or the empty set of no _assigns_ | ||
clause is specified. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, wasn't there some code that would automatically determine what to havoc, and, therefore, in a way, the memory locations being written to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is indeed an inference pass for loop contracts in case there isn't a user-specified clause in the loop contract.
I'll make the distinction.
We don't have inference for function contracts. Since we ultimately want to have function contracts as standalone entities we wouldn't know what instructions to infer the assigned memory locations from.
specified by each of its assigns clauses, or the empty set of no _assigns_ | ||
clause is specified. | ||
|
||
The assigns clause may only refer to locations that exist in the calling context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a function is called in exactly two places, and both of them have a procedure-local variable x
in scope, can I use &x
in the assigns clause?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
syntactically the targets must be expressions of function parameters or globals, but they can surely resolve to that procedure local &x
at some call-site. However I'm not sure what it would mean to modify the address of a procedure local variable. Modifying the value stored at this address, yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies, I should have used x
instead of &x
. I don't see where the documentation makes explicit that such expressions must be function parameters or globals?
src/ansi-c/c_typecheck_code.cpp
Outdated
bool is_empty_type = target.type().id() == ID_empty; | ||
bool is_assignable_typedef = | ||
target.type().get(ID_C_typedef) == CPROVER_PREFIX "assignable_t"; | ||
// only allow void type if it is the typedef CPROVER_PREFIX "assignable_t" | ||
if(target.type().id() == ID_empty && !is_assignable_typedef) | ||
{ | ||
error().source_location = target.source_location(); | ||
error() << "void-typed expressions not allowed as assigns clause targets" | ||
<< eom; | ||
throw 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we actually allowed void
-typed expressions? Would there really be anything wrong in doing that? Wouldn't that allow us to get rid of __CPROVER_assignable_t
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type is meant to documents the fact that the function specifies sets of assignable locations.
But mostly it is used to distinguish functions that specify assigns clauses from other void functions and process them differently when dynamic frames are involved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not convinced that a typedef
is the best way to address this problem. The number of permitted functions is very small, can't we just specifically check for those in each place that requires checking?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Today we only provide 4 built-ins returning __CPROVER_assignable_t
,
but the dynamic frames PRs builds on top of this to allow users to write their own functions returning __CPROVER_assignable_t
. These user-defined functions can call other functions returning __CPROVER_assignable_t
(built-in or user-defined), so the set is not necessarily small.
I intended __CPROVER_assignable_t
as an opaque type that represents a set of memory locations. It is defined as void in the front-end, and gets mapped to an actual representation by the instrumentation pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My two cents, but I like the __CPROVER_assignable_t
approach for representing sets of memory locations.
src/ansi-c/c_typecheck_expr.cpp
Outdated
if(identifier == CPROVER_PREFIX "typed_target") | ||
{ | ||
if(expr.arguments().size() != 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The remainder of the code in this function sets out a poor example, but: could the body of this if
be factored out into a function of its own?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, will do.
src/ansi-c/c_typecheck_expr.cpp
Outdated
error().source_location = f_op.source_location(); | ||
error() << "expected 1 argument for " << CPROVER_PREFIX "typed_target" | ||
<< " found " << expr.arguments().size() << eom; | ||
throw 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above: invalid_source_file_exceptiont
src/ansi-c/c_typecheck_expr.cpp
Outdated
error().source_location = arg0.source_location(); | ||
error() << "argument of " << CPROVER_PREFIX "typed_target" | ||
<< "must be assignable" << eom; | ||
throw 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above: invalid_source_file_exceptiont
src/ansi-c/c_typecheck_expr.cpp
Outdated
error().source_location = arg0.source_location(); | ||
error() << "sizeof not defined for argument of " | ||
<< CPROVER_PREFIX "typed_target" | ||
<< " of type " << to_string(arg0.type()) << eom; | ||
throw 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above: invalid_source_file_exceptiont
20ee9e8
to
bcf61d0
Compare
bcf61d0
to
70acafe
Compare
^SIGNAL=0$ | ||
-- | ||
-- | ||
This test checks that the front end rejects __CPROVER_is_freed in preconditions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add that the intention of the code is to resize an array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/ansi-c/c_typecheck_expr.cpp
Outdated
if(expr.arguments().size() != 1) | ||
{ | ||
std::ostringstream error_message; | ||
error_message << CPROVER_PREFIX "is_freeable expects one operand; " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these errors checked in regression? I don't remember seeing them in the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tests added
src/ansi-c/c_typecheck_code.cpp
Outdated
bool is_empty_type = target.type().id() == ID_empty; | ||
bool is_assignable_typedef = | ||
target.type().get(ID_C_typedef) == CPROVER_PREFIX "assignable_t"; | ||
// only allow void type if it is the typedef CPROVER_PREFIX "assignable_t" | ||
if(target.type().id() == ID_empty && !is_assignable_typedef) | ||
{ | ||
error().source_location = target.source_location(); | ||
error() << "void-typed expressions not allowed as assigns clause targets" | ||
<< eom; | ||
throw 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My two cents, but I like the __CPROVER_assignable_t
approach for representing sets of memory locations.
|
||
In a function contract: | ||
```c | ||
int foo(char *arr1, char *arr2, size_t size) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
e2b70d7
to
063a9a0
Compare
CORE | ||
main.c | ||
--apply-loop-contracts | ||
^main.c.* error: frees clause target must be a pointer-typed expression or a call to a function returning void$ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the error message says the name and the type of the parameter an user tried to use with __CPROVER_frees
? It might be really helpful for debugging.
while(i <= size) | ||
// clang-format off | ||
__CPROVER_assigns(i, arr[2], __CPROVER_POINTER_OBJECT(arr)) | ||
__CPROVER_frees(arr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if I call __CPROVER_frees
with a function returning void
, but with no pointers in it. For example,
void foo()
{
int x;
}
...
__CPROVER_frees(foo())
typecheck_spec_frees(code_type.frees()); | ||
for(auto &frees : code_type.frees()) | ||
{ | ||
lambda_exprt lambda{temporary_parameter_symbols, frees}; | ||
lambda.add_source_location() = frees.source_location(); | ||
frees.swap(lambda); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: we might want to throw a simple warning saying there is current no implementation for it.
|
||
## Frees clause related predicates | ||
|
||
The predicate: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The predicate: |
of the `free` function from `stdlib.h`, that is if and only if the pointer | ||
points to a valid dynamically allocated object and has offset zero. | ||
|
||
The predicate: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The predicate: |
063a9a0
to
0dfb3d7
Compare
0dfb3d7
to
f23c73a
Compare
## Specifying parametric sets of freeable pointers using C functions | ||
|
||
Users can define parametric sets of freeable pointers by writing functions that | ||
return the built-in type void and call the built-in function |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
|
||
## Frees clause related predicates | ||
|
||
The predicate: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The predicate: |
of the `free` function from `stdlib.h`, that is if and only if the pointer | ||
points to a valid dynamically allocated object and has offset zero. | ||
|
||
The predicate: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The predicate: |
f23c73a
to
66a7c5d
Compare
74c89b3
to
d236601
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok in principle, but I'm confused on some occasions. Please clarify.
# Frees Clauses | ||
|
||
A _frees clause_ allows the user to specify a set of pointers that may be freed | ||
by a function. A function contract may have zero or more frees clauses. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that just this function that has the contract attached to it, or any function that may (transitively) be called by the function that has the contract?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
transitively
| __CPROVER_freeable(lvalue-expr) | ||
``` | ||
|
||
A frees clause target must be eiter: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/eiter/either/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
The set of pointers specified by the frees clause of the contract is interpreted | ||
at the function call-site. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At each call site of the function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
at each call site where the contract is checked or replaced
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
src/ansi-c/c_typecheck_expr.cpp
Outdated
expr.source_location()}; | ||
} | ||
typecheck_function_call_arguments(expr); | ||
return nil_exprt(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What to make of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using the same approach as the one used with is_fresh : return nil_exprt()
so that the original expression stays in place. When do_special_function
returns a non-nil expression it replaces the original expression:
exprt tmp=do_special_functions(expr);
if(tmp.is_not_nil())
expr.swap(tmp);
else
typecheck_function_call_arguments(expr);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removing this completely since it seems like declaring the functions in cprover_builtin_headers.h
and ansi_c_internal_additions.cpp
suffices to have them type-checked. I don't understand why this was done like this for is_fresh
src/ansi-c/c_typecheck_expr.cpp
Outdated
expr.source_location()}; | ||
} | ||
typecheck_function_call_arguments(expr); | ||
return nil_exprt(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... and this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above. I am just following the pattern set by someone else with is_fresh
| TOK_CPROVER_FREES '(' ')' | ||
{ | ||
$$=$1; | ||
set($$, ID_C_spec_frees); | ||
parser_stack($$).add_to_operands(exprt(ID_target_list)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this case even necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, this is meant to allows for empty frees clauses to be explicitly written in contracts instead of having to be forcibly omitted.
src/goto-programs/goto_convert.cpp
Outdated
auto frees = static_cast<const unary_exprt &>(code.find(ID_C_spec_frees)); | ||
if(frees.is_not_nil()) | ||
{ | ||
PRECONDITION(loop->is_goto()); | ||
loop->condition_nonconst().add(ID_C_spec_frees).swap(frees.op()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation suggested this was only permitted on function pre/post-conditions and not on loop invariants?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, this is a leftover
Add the following to the front-end: - add `__CPROVER_frees` clause for function contracts - add `__CPROVER_freeable` built-in function - add `__CPROVER_is_freeable` built-in predicate - add `__CPROVER_was_freed` built-in predicate Effective support for the new clause type and related predicates will come in a later PR.
d236601
to
5982243
Compare
This is extracted from the dynamic frames PR #6887 and contains the front-end modifications only.
Add the following to the front-end:
__CPROVER_frees
clause for function and loop contractsin the parser and internal data structures
__CPROVER_freeable
built-in function__CPROVER_is_freeable
built-in predicate__CPROVER_is_freed
built-in predicateEffective support for the new clause type and related predicates will come in the next PR.