You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As part of the function contracts implementation we require a notion of history variables, aka old.
As a running example we shall use this simple function (modify) and harness (main):
fnmodify(ptr:&mutu32){*ptr += 1}#[kani::proof]fnmain(){let _ = Box::new(());letmut i = kani::any();modify(&mut i);}
Prereqs
We have access to history variables in CBMC, via __CPROVER_old.
Attempt 1: Rewrite a kani::old function via GotoHook
This method exposes the function below as part of the kani library and installs a hook in the code generation that implements this function call as __CPROVER_old(*t).
We do this so we can support arbitrary code in the condition and Rust deals with parsing, type checking etc for us. The CBMC level contract we then generate is __CPROVER_ensures(modify_ensures_1(ptr, &__CPROVER_return_value)). We need to use this encoding, because our generated function may contain control flow, which is generated if the contract calls functions or uses the short circuiting && or ||. Control flow is not allowed directly in the CBMC contract, but is allowed in the called function which is why we can't (easily) inline the body of modify_ensures_1.
We would need to inline the generated function directly into the call to __CPROVER_ensures and rewrite all control flow to eliminate jumps.
Attempt 2: Pass all old arguments to the generated function
In the second attempt we instead always provide all history values as old_<var name>. In the example this would change the contract to #[kani::ensures(*old_ptr == *ptr - 1)] and generate the following function
And the CBMC contract __CPROVER_ensures(modify_ensures_1(ptr, __CPROVER_old(ptr), &__CPROVER_return_value)).
This also does not work, because unfortunately __CPROVER_old makes shallow copies. In this case it would copy the value of the pointer, but not the value behind the pointer and making this contract fail.
Proposal 1: Expression extraction
We could expand the idea from Attempt 2, but instead of always working on the arguments work on arbitrary (lvalue) expressions we extract during the macro invocation.
The contract would look more similar to Attempt 1: #[kani::ensures(old(ptr) == *ptr - 1)], but this time the expression in the old() call is extracted, stored and replaced by a generated_variable. We then generate a function that expects the result of that old() call to be passed in addition to the function arguments.
And generate the CBMC contract __CPROVER_ensures(modify_ensures_1(ptr, __CPROVER_old(*ptr), &__CPROVER_return_value).
This has a couple of problems actually. The first one is types. While I just gave old_1u32 here, when this function is generated (in a macro) we don't actually have access to the type of *ptr. We could get around this by instead generating a function that wraps a closure which closes over the old expressions to get type inference, e.g.
The second drawback is that we can only support simple expressions inside of old. In particular we cannot support function calls easily as they require type inference, monomorphization etc. But they are needed because for instance our vec-map example always uses function call conditions, e.g. !old(self.contains_key(&key)).
Proposal 2: Inlining spec functions
Straightforward: Just do what I said before is hard and transform the body of modify_ensures_1 into an expression and inline it.
This is actually still not enough to enable function calls in old, because CBMC does not allow it, see diffblue/cbmc#7802
Proposal 3: Abandon CBMC
Instead of using CBMC we can solve this issue on the Kani side but that means we need to reimplement contract enforcement. The easiest way to achieve it is through lots of code generation plus function replacement in the compiler. When we encounter a contract instead of just generating a function for the contract, we can instead generate a new function that implements the contract's enforcement (or substitution). old is handled by parsing the condition with syn and then replacing all invocations of old with a temporary variable. The enforcement function has the same signature as the function on which the contract is placed. In its body we first discharge all assumes, then evaluate the old expressions and store them in the aforementioned variables, then we call the original function, then check all asserts. Finally we link the enforcement function to the original function using a kanitool attribute that carries the name of the replacement function. Example below for the contract #[kani::ensures(old(*ptr) == *ptr - 1)]
fnmodify_copy_1(ptr:&mutu32){*ptr += 1}fnmodify_check_1(ptr:&mutu32){let old_1 = *ptr;let result = modify_copy_1(ptr);
kani::assert(old_1 == *ptr - 1);
result
}#[kanitool::checked_with = "modify_check_1"]fnmodify(ptr:&mutu32){*ptr += 1}
The benefit of this approach is that this supports arbitrary expressions inside old though they must respect borrowing rules. That however is fine, as users can simply use clone if they want to avoid references.
The easiest way to make implement the enforcement in a harness then is to add a kani::for_contract(modify) attribute for harnesses that instructs the compiler to replace occurrences of modify with the function mentioned in checked_by on modify. This way we need no additional enforcement mechanism and the driver can simply invoke the harness.
The only exception here is assigns clauses. I see no easy way to handle them in Kani, but they can simply be substituted and dispatched to CBMC as before.
Note: I have actually already implemented this last option and it works well for contract checking on vec-map, including supporting arbitrary old expressions.
The text was updated successfully, but these errors were encountered:
As part of the function contracts implementation we require a notion of history variables, aka
old
.As a running example we shall use this simple function (
modify
) and harness (main
):Prereqs
We have access to history variables in CBMC, via
__CPROVER_old
.Attempt 1: Rewrite a
kani::old
function viaGotoHook
This method exposes the function below as part of the
kani
library and installs a hook in the code generation that implements this function call as__CPROVER_old(*t)
.This would make a contract for our example be
#[kani::ensures(kani::old(ptr) == *ptr - 1)]
This approach did not work, because the following constraints cannot be (easily) unified.
__CPROVER_ensures(modify_ensures_1(ptr, &__CPROVER_return_value))
. We need to use this encoding, because our generated function may contain control flow, which is generated if the contract calls functions or uses the short circuiting&&
or||
. Control flow is not allowed directly in the CBMC contract, but is allowed in the called function which is why we can't (easily) inline the body ofmodify_ensures_1
.__CPROVER_old
is only allowed to be called directly inside__CPROVER_ensures
, not anywhere else and thus also illegal inmodify_ensures_1
. See__CPROVER_old
is ignored when called form a spec function diffblue/cbmc#7775We would need to inline the generated function directly into the call to
__CPROVER_ensures
and rewrite all control flow to eliminate jumps.Attempt 2: Pass all old arguments to the generated function
In the second attempt we instead always provide all history values as
old_<var name>
. In the example this would change the contract to#[kani::ensures(*old_ptr == *ptr - 1)]
and generate the following functionAnd the CBMC contract
__CPROVER_ensures(modify_ensures_1(ptr, __CPROVER_old(ptr), &__CPROVER_return_value))
.This also does not work, because unfortunately
__CPROVER_old
makes shallow copies. In this case it would copy the value of the pointer, but not the value behind the pointer and making this contract fail.Proposal 1: Expression extraction
We could expand the idea from Attempt 2, but instead of always working on the arguments work on arbitrary (lvalue) expressions we extract during the macro invocation.
The contract would look more similar to Attempt 1:
#[kani::ensures(old(ptr) == *ptr - 1)]
, but this time the expression in theold()
call is extracted, stored and replaced by a generated_variable. We then generate a function that expects the result of thatold()
call to be passed in addition to the function arguments.And generate the CBMC contract
__CPROVER_ensures(modify_ensures_1(ptr, __CPROVER_old(*ptr), &__CPROVER_return_value)
.This has a couple of problems actually. The first one is types. While I just gave
old_1
u32
here, when this function is generated (in a macro) we don't actually have access to the type of*ptr
. We could get around this by instead generating a function that wraps a closure which closes over theold
expressions to get type inference, e.g.This will cause Rustc to generate code for a closure for us like so:
We can then ignore
modify_ensures_1
and generate a CBMC contract from the closure like soThe second drawback is that we can only support simple expressions inside of
old
. In particular we cannot support function calls easily as they require type inference, monomorphization etc. But they are needed because for instance ourvec-map
example always uses function call conditions, e.g.!old(self.contains_key(&key))
.Proposal 2: Inlining spec functions
Straightforward: Just do what I said before is hard and transform the body of
modify_ensures_1
into an expression and inline it.This is actually still not enough to enable function calls in
old
, because CBMC does not allow it, see diffblue/cbmc#7802Proposal 3: Abandon CBMC
Instead of using CBMC we can solve this issue on the Kani side but that means we need to reimplement contract enforcement. The easiest way to achieve it is through lots of code generation plus function replacement in the compiler. When we encounter a contract instead of just generating a function for the contract, we can instead generate a new function that implements the contract's enforcement (or substitution).
old
is handled by parsing the condition withsyn
and then replacing all invocations ofold
with a temporary variable. The enforcement function has the same signature as the function on which the contract is placed. In its body we first discharge allassume
s, then evaluate theold
expressions and store them in the aforementioned variables, then we call the original function, then check allassert
s. Finally we link the enforcement function to the original function using akanitool
attribute that carries the name of the replacement function. Example below for the contract#[kani::ensures(old(*ptr) == *ptr - 1)]
The benefit of this approach is that this supports arbitrary expressions inside
old
though they must respect borrowing rules. That however is fine, as users can simply useclone
if they want to avoid references.The easiest way to make implement the enforcement in a harness then is to add a
kani::for_contract(modify)
attribute for harnesses that instructs the compiler to replace occurrences ofmodify
with the function mentioned inchecked_by
onmodify
. This way we need no additional enforcement mechanism and the driver can simply invoke the harness.The only exception here is
assigns
clauses. I see no easy way to handle them in Kani, but they can simply be substituted and dispatched to CBMC as before.Note: I have actually already implemented this last option and it works well for contract checking on
vec-map
, including supporting arbitraryold
expressions.The text was updated successfully, but these errors were encountered: