CBMC
|
Back to Code Contracts User Documentation
An assigns clause allows the user to specify a set of locations that may be assigned to by a function or the body of a loop:
Where targets
has the following syntax:
The set of locations writable by a function is the union of the sets of locations specified by its assigns clauses, or the empty set if no assigns clause is specified. While, in general, an assigns clause could be interpreted with either writes or modifies semantics, this design is based on the former. This means that memory not captured by an assigns clause must not be assigned to by the given function, even if the value(s) therein are not modified.
For function contracts, the condition and target expressions in the assigns clause may only involve function parameters, global variables or type identifiers (in sizeof
or cast expressions). The target expression must be free of function calls and side-effects. The condition expression may contain calls to side-effect-free functions.
For a loop contract, the condition and target expressions in the assigns clause may involve any identifier that is in scope at loop entry (parameters of the surrounding function, local variables, global variables, type identifiers in sizeof
or cast expressions, etc.). In case of nested loops, the assigns clause of the outer loop should contain all assigns targets of the inner loops. The target expression must be free of function calls and side-effects. The condition expression may contain calls to side-effect-free functions.
Roughly speaking, lvalues are expressions that are associated with a memory location, the address of which can be computed using the address-of operator &
.
Examples of lvalues are: x
if x
is either a global or local variable identifier, *ptr
if ptr
is a pointer expression, ptr[i]
or ptr + i
if ptr
is pointer variable or an array and i
is an integer expression, etc.
Examples of non-lvalues: literal constants like 0
, 1
, ..., arithmetic expressions like x + y
when x
and y
are both arithmetic variables, function pointer expressions, etc.
An lvalue target expr
with a complete type expr_t
specifies that the range starting at &expr
and of size sizeof(expr_t)
bytes is assignable.
Lvalues can also be wrapped in __CPROVER_typed_target
with the same meaning: for an lvalue expr
with a complete type expr_t
, __CPROVER_typed_target(expr)
specifies that the range of sizeof(expr_t)
bytes starting at &expr
is assignable:
In order to specify that a memory location the contents of which is interpreted as a pointer by the program is assignable, one must use the notation __CPROVER_assigns(ptr)
or the equivalent __CPROVER_assigns(__CPROVER_typed_target(ptr))
, as opposed to the slice operators __CPROVER_object_whole
, __CPROVER_object_from
, or __CPROVER_object_upto
. This ensures that during call-by-contract replacement the memory location gets turned into a non-deterministic pointer.
For instance:
Given a pointer ptr
pointing into some object (possibly at some non-zero offset), __CPROVER_object_upto(ptr, size)
specifies that the range of size
bytes starting at ptr
is assignable.
The value of size
must such that the range does not exceed the object's boundary, i.e. size <= __CPROVER_OBJECT_SIZE(ptr) - __CPROVER_POINTER_OFFSET(ptr)
must hold (otherwise an assertion violation will occur and make the whole analysis fail).
In the example below, the struct vect_t
, its data
array and an exta hidden byte are packed together in a single object by the vec_alloc
function. The vec_clear
function can only assign vec->size
bytes starting from vec->data
. As a result the assignments to vec->size
and the hidden byte fail the verification.
Given a pointer ptr
pointing into some object (possibly at some non-zero offset), __CPROVER_object_from(ptr)
specifies that the range of bytes starting at ptr
and of size __CPROVER_OBJECT_SIZE(ptr) - __CPROVER_POINTER_OFFSET(ptr)
is assignable.
Revisiting our previous example, changing the target to __CPROVER_object_from(vec->data)
still rejects the assignment to vec->size
, but allows the assignment to the hidden byte which is located after the data array in memory.
Given a pointer ptr
pointing into some object (possibly at some non-zero offset), __CPROVER_object_whole(ptr)
specifies that the range of bytes of size __CPROVER_OBJECT_SIZE(ptr)
starting at address ptr - __CPROVER_POINTER_OFFSET(ptr)
is assignable:
If the pointer has a positive offset into some object, the range includes bytes that are in the object before the address pointed to by ptr
. Revisiting our example one last time, changing the target to __CPROVER_object_whole(vec->data)
allows the function (perhaps mistakenly) to assign to vec->size
, the whole array of size vec->size
pointed to by vec->data
and the hidden byte.
For a function contract, the memory locations storing function parameters are considered as being local to the function and are hence always assignable.
For a loop contract, the parameters of the enclosing function are not considered local to the loop and must be explicitly added to the loop to become assignable.
Inductive data structures are not supported yet in assigns clauses.
Each target listed in an assigns clause defines a conditionally assignable range of bytes represented by the following triple:
where:
start_address
is the start address of the range of bytes,size
is the size of the range in number of bytes,is_writable
is true iff the target's condition
holds and __CPROVER_w_ok(start_address, size)
holds at the program location where the clause is interpreted: right before function invocation for function contracts and at loop entry for loops;For contract enforcement, assigns clause targets are turned into checks, to verify that the function only assigns locations allowed by the assigns clause.
For contract replacement, assigns clause targets are turned into havoc statements, to model the non-deterministic behaviour specified by the contract.
In order to determine whether a function (or loop) complies with the assigns clause of the contract, the body of the function (or loop) is instrumented with assertion statements before each statement which may write to memory (e.g., an assignment). These assertions check that the location about to be assigned to is among the targets specified by the assigns clauses.
For example, consider the following implementation of sum
function.
Assignable locations for the sum
function are the locations specified with __CPROVER_assigns
, together with any location storing a function parameter, or any location that is locally stack- or heap-allocated as part of function (or loop) execution.
In the case of sum
that is *out
and result
. Each assignment will be instrumented with an assertion to check that the target of the assignment is one of those options.
Assuming the assigns clause of the contract correctly captures the set of locations assigned to by a function (checked during contract enforcement), CBMC will use the contract's Requires & Ensures Clauses, and its assigns clause to generate a sound abstraction of the function behaviour from the contract.
Given the contract:
Function calls f(args)
get replaced with a sequence of instuctions equivalent to:
Where R[args/params]
, A[args/params]
, E[args/params]
denote the contract clause expressions rewritten by substituting function parameters with the argyments passed at the call site.
In our example, consider that a function foo
may call sum
.
CBMC will use the function contract in place of the function implementation wherever the function is called.
When loop invariant clauses are specified but loop assigns are not, CBMC will infer loop assigns clauses and use them to apply loop contracts. The idea of the inference is to include all the left hand side of assignments in the loop.
For example, in the loop in the following function, we assume that only the loop invariant i <= SIZE
is specified. Then CBMC will infer loop assigns targets i
, j
and __CPROVER_object_whole(b)
for the loop.
The inference algorithm consist of three stages:
We do the function inlining first so that we can infer those assigns targets hidden in the function call, for example, j
is assigned to in set_j
.
Then we collect all targets of assignments in the loop after inlining. In the test_loop_assigns_inference
example, there are five assignments in the loop.
n = &i
, with assign target n
. n
will not be included in the inferred set as it is a loop local.*n++
, with assign target *n
. We add i
to the inferred set as &i
aliasing n
according to the above assignment.k = i + 1
, with assign target k
. k
is also a loop local, and will not be added to the inferred set.j = i
with assign target j
. So we add j
to the inferred set.b[j] = 1
with assign target b[j]
. So we add b[j]
to the inferred set.At last, we widen b[j]
to __CPROVER_object_whole(b)
as its index j
is non constant.
The main limitation of the inference algorithm is that the local-may-alias analysis we use is field insensitive, hence it is inaccurate in the cases with struct fields.
As an example, for assignment ptr = box.ptr
, we cannot determine that ptr
aliases box.ptr
. And hence if we later write to *ptr
, we will fail to infer the assigns target __CPROVER_object_whole(box.ptr)
.
However, the failed inference does not result in unsoundness. That is, CBMC will report assignability-checks failure when the inferred assigns clauses are not accurate.