A potential bug in the induction strategy of Frama-C 24.0 - frama-c

I am working on the following proof and the invariant result_val is proved with an induction strategy on i using begin as the base case.
The sup case is trying to prove true which holds trivially using Frama-C 24.0. But when I switch to 25.0, it tries to prove a seemingly more complicated condition, which looks closer to a correct inductive inference because it did the weakest precondition computation explicitly.
However, all SMT solvers I tried cannot prove the condition generated by Frama-C 25.0.
I am a bit worried about the correctness of version 24.0's result because using true as the inductive proof goal seems to be unlikely. Can anyone hint to me at what happened? Is that a bug in 24.0 or just some difference in the implementation?
#include <stdbool.h>
#define SIZE 1000
bool data[SIZE] ;
/*#
logic integer count(integer begin, integer end)=
begin >= end ? 0 : (data[begin]==true) ? count(begin+1, end)+1 : count(begin+1, end);
*/
/*#
requires SIZE > begin >= 0;
requires SIZE >= end >= 0;
requires begin <= end;
assigns \nothing;
ensures \result == count(begin, end);
*/
unsigned int occurrences_of(int begin, int end)
{
unsigned int result = 0;
/*#
loop invariant i_bound: begin <= i <= end;
loop invariant result_bound: 0 <= result <= i-begin;
loop invariant result_val: result == count(begin, i);
loop assigns i, result;
loop variant end-i;
*/
for (unsigned int i = begin; i < end; ++i){
result += (data[i] == true) ? 1 : 0;
}
return result;
}
Below is the result from Frama-c 24.0
Proof:
Goal Invariant 'result_val' (preserved) (Induction: proved)
+ Goal Induction (Base) (proved)
+ Goal Induction (Induction (sup)) (proved)
+ Goal Induction (Induction (inf)) (proved)
Qed.
--------------------------------------------------------------------------------
Goal Induction (Induction (sup)):
Prove: true.
Below is the result from Frama-c 25.0
--------------------------------------------------------------------------------
Proof:
Goal Invariant 'result_val' (preserved) (Induction: pending)
+ Goal Induction (Base) (proved)
+ Goal Induction (Induction (sup)) (pending)
+ Goal Induction (Induction (inf)) (proved)
End.
--------------------------------------------------------------------------------
Goal Induction (Induction (sup)):
Let x_0 = to_uint32(end#L1).
Let x_1 = to_uint32(tmp#L12).
Let x_2 = data#L1[i#L6].
Let x_3 = result#L6.
Let x_4 = result#L13.
Let x_5 = to_uint32(1 + i#L6).
Assume {
Have: begin#L1 < i#L6.
Have: i#L6 <= end#L1.
Have: i#L6 < x_0.
Have: 0 <= x_3.
Have: x_5 <= end#L1.
Have: begin#L1 <= x_5.
Have: (begin#L1 + x_3) <= i#L6.
Have: (begin#L1 + x_4) <= x_5.
Have: is_uint32(i#L6).
Have: is_bool(x_2).
Have: is_uint32(x_3).
Have: if (x_2 = 1) then (tmp#L12 = 1) else (tmp#L12 = 0).
Have: forall i_0 : Z. let x_6 = L_count(data#L1, begin#L1, i_0) in
let x_7 = to_uint32(1 + i_0) in let x_8 = to_uint32(x_1 + x_6) in
let x_9 = data#L1[i_0] in ((i_0 <= end#L1) -> ((begin#L1 <= i_0) ->
((i_0 < i#L6) -> ((i_0 < x_0) -> ((0 <= x_6) -> ((x_7 <= end#L1) ->
((begin#L1 <= x_7) -> (((begin#L1 + x_6) <= i_0) ->
(((begin#L1 + x_8) <= x_7) -> (is_uint32(i_0) -> (is_bool(x_9) ->
(is_uint32(x_6) ->
((if (x_9 = 1) then (tmp#L12 = 1) else (tmp#L12 = 0)) ->
(L_count(data#L1, begin#L1, x_7) = x_8)))))))))))))).
[...]
Stmt { L6: }
Stmt { tmp = tmp_0; }
Stmt { L12: result = x_4; }
Stmt { L13: }
}
Prove: L_count(data#L1, begin#L1, x_5) = x_4.
Goal id: typed_occurrences_of_loop_invariant_result_val_preserved
Short id: occurrences_of_loop_invariant_result_val_preserved
--------------------------------------------------------------------------------
Prover Alt-Ergo 2.4.2: Timeout (Qed:52ms) (10s).

A bug on the typing of the induction tactic was indeed fixed between Frama-C 24 and 25 (https://git.frama-c.com/pub/frama-c/-/commit/6058453cce2715f7dcf9027767559f95fb3b1679). And the symptom was indeed that the tactic could generate ill-typed formulas with true instead of a term.
Proving this example in not that easy. For two main reasons:
the function and the definition work in the opposite directions,
the definition does not have an optimal expression for reasoning.
However, one can write a lemma function to solve the problem:
#include <stdbool.h>
#define SIZE 1000
bool data[SIZE] ;
/*#
logic integer count(integer begin, integer end)=
begin >= end ? 0 : ((data[begin]==true) ? count(begin+1, end)+1 : count(begin+1, end));
*/
/*# ghost
/# requires begin < end ;
assigns \nothing ;
ensures count(begin, end) == ((data[end-1]==true) ? count(begin, end-1)+1 : count(begin, end-1));
#/
void lemma(bool* d, int begin, int end){
/# loop invariant begin <= i < end ;
loop invariant count(i, end) == ((data[end-1]==true) ? count(i, end-1)+1 : count(i, end-1));
loop assigns i ;
loop variant i - begin ;
#/
for(int i = end-1 ; i > begin ; i--);
}
*/
/*#
requires SIZE > begin >= 0;
requires SIZE >= end >= 0;
requires begin <= end;
assigns \nothing;
ensures \result == count(begin, end);
*/
unsigned int occurrences_of(int begin, int end)
{
unsigned int result = 0;
/*#
loop invariant i_bound: begin <= i <= end;
loop invariant result_bound: 0 <= result <= i-begin;
loop invariant result_val: result == count(begin, i);
loop assigns i, result;
loop variant end-i;
*/
for (unsigned int i = begin; i < end; ++i){
result += (data[i] == true) ? 1 : 0;
//# ghost lemma(data, begin, i+1);
}
return result;
}
I'd suggest to use the following definition:
/*#
logic integer count(integer begin, integer end)=
begin >= end ? 0 : ((data[end-1]==true) ? 1 : 0) + count(begin, end-1);
*/
It works in the same direction as the function and avoids the duplication of the term count(begin, end-1) which makes reasoning easier.

Related

Failed to verifying the occurrence of a value in an array with logic function

I tried to use Frama-c to prove the occurrence of true value in an array that is the same as a integer which used to record the number of true values. But the prove failed if I want to change some values false to true.
Is there any method to solve this problem?
(Frama-c 25.0 / Alt-Ergo 2.4.2 / CVC4 1.8 / Z3 4.8.6)
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define SIZE 150
bool table[SIZE] = {true};
int true_counter;
/*#
logic integer count_true(integer idx) =
idx<=0 ? 0 :
(table[idx-1]==true ? count_true(idx-1) + 1 : count_true(idx-1));
*/
/*#
requires table[0] == false;
requires true_counter == count_true(SIZE);
assigns table[0], true_counter;
ensures table[0] == true;
ensures true_counter == \old(true_counter) + 1;
*/
void ATEST(void) {
int countBF = 0;
int countAF = 0;
/*#
loop invariant 0 <= idx <= SIZE;
loop invariant 0 <= countBF == count_true(idx) <= idx;
loop assigns idx, countBF;
loop variant SIZE - idx;
*/
for(int idx=0; idx<SIZE; idx++) {
if(table[idx] == true) countBF += 1;
}
//# assert true_counter == countBF;
table[0] = true;
true_counter += 1;
/*#
loop invariant 0 <= idx <= SIZE;
loop invariant 0 <= countAF == count_true(idx) <= idx;
loop assigns idx, countAF;
loop variant SIZE - idx;
*/
for(int idx=0; idx<SIZE; idx++) {
if(table[idx] == true) countAF += 1;
}
//# assert true_counter == countAF;
//# assert countAF == countBF + 1;
}
[kernel] Parsing test.c (with preprocessing)
[rte:annot] annotating function ATEST
[wp] 33 goals scheduled
[wp] [Failed] Goal typed_ATEST_assert_2
Z3 4.8.6: Unknown (Qed:8ms) (cached)
CVC4 1.8: Timeout (Qed:8ms) (10s) (cached)
Alt-Ergo 2.4.2: Timeout (Qed:8ms) (10s) (cached)
[wp] [Cache] found:17
[wp] Proved goals: 32 / 33
Qed: 26 (0.84ms-7ms-24ms)
Alt-Ergo 2.4.2: 6 (7ms-13ms-27ms) (629) (interrupted: 1) (cached: 7)
CVC4 1.8: 4 (20ms-28ms-30ms) (11375) (interrupted: 1) (cached: 5)
Z3 4.8.6: 4 (10ms-20ms) (46252) (unknown: 1) (cached: 5)
The problem is that such a reasoning is not that direct. Here is what happen: when we write cell 0, WP creates a new memory whose properties are directly related to the old memory. The loop invariant of the first loop gives to the solver some information about the original memory, the loop invariant of the second loop gives information about the new memory. But for establishing the link between the two we need to learn something like "since only on value has changed, the remaining part has the same number of occurrences". Which is not possible with most SMT solvers.
For this particular example, we can be brutal and directly push the information in the invariant of the second loop:
/*#
loop invariant 0 <= idx <= SIZE;
loop invariant 0 <= countAF == count_true(idx) <= idx;
// added:
loop invariant idx >= 1 ==> countAF == 1 + count_true{Pre}(idx) <= idx;
loop assigns idx, countAF;
loop variant SIZE - idx;
*/
for(int idx=0; idx<SIZE; idx++) {
if(table[idx] == true) countAF += 1;
}
Which allows to create the link between the number of occurrences in the pre-state with the current number of occurrences. A more elegant solution is to dedicate a bit of ghost code so that it applies to any location:
/*#
requires 0 <= loc < SIZE ;
requires table[loc] == false;
requires true_counter == count_true(SIZE);
assigns table[loc], true_counter;
ensures table[loc] == true;
ensures true_counter == \old(true_counter) + 1;
*/
void ATEST(int loc) {
int countBF = 0;
int countAF = 0;
/*#
loop invariant 0 <= idx <= SIZE;
loop invariant 0 <= countBF == count_true(idx) <= idx;
loop assigns idx, countBF;
loop variant SIZE - idx;
*/
for(int idx=0; idx<SIZE; idx++) {
if(table[idx] == true) countBF += 1;
}
//# assert true_counter == countBF;
table[loc] = true;
true_counter += 1;
/*# ghost
/# loop invariant 0 <= i <= loc ;
loop invariant count_true(i) == count_true{Pre}(i);
loop assigns i;
loop variant loc - i ;
#/
for(int i = 0 ; i < loc ; i++);
/# loop invariant loc < i <= SIZE ;
loop invariant count_true(i) == 1 + count_true{Pre}(i);
loop assigns i;
loop variant SIZE - i ;
#/
for(int i = loc+1 ; i < SIZE ; i++);
*/
/*#
loop invariant 0 <= idx <= SIZE;
loop invariant 0 <= countAF == count_true(idx) <= idx;
loop assigns idx, countAF;
loop variant SIZE - idx;
*/
for(int idx=0; idx<SIZE; idx++) {
if(table[idx] == true) countAF += 1;
}
}
On can also express lemmas to handle the problem. For example the following lemmas can provide enough information to solvers so that they can finish the proof:
/*# lemma same_count{L1,L2}:
\forall integer size; 0<= size < SIZE ==>
(\forall integer i; 0 <= i < size ==> \at(table[i], L1) == \at(table[i],L2)) ==>
count_true{L1}(size) == count_true{L2}(size);
*/
/*# lemma same_but_one{L1,L2}:
\forall integer size; 0 <= size < SIZE ==>
\forall integer i_diff; 0 <= i_diff < size ==>
(\forall integer i; 0 <= i < size && i != i_diff ==> \at(table[i],L1) == \at(table[i],L2))
&& \at(table[i_diff],L1) == false && \at(table[i_diff],L2) == true
==>
count_true{L1}(size) + 1 == count_true{L2}(size);
*/
The first lemma states that an unchanged memory region keeps the same number of occurrences as before. The second states that when in a state a cell has true while in another it is false and all remaining cells are the same, the number of occurrences changes by one. These two lemmas can be verified via the induction tactic (see for example https://stackoverflow.com/a/73796135/4628125).
These two lemmas can be used directly by the solvers to get a proof for any location.

Failed to establish an invariant requiring the knowledge of the content of an array

I try to use Frama-C to prove the invariant below. With the precondition that the array clean has at least one "true" in the first half, the loop should terminate before the last iteration and hence i will never be increased to BLOCK_NUM (=1000). However, Frama-C seems to ignore the content of clean and failed to establish the loop invariant. Does anyone know how to fix this proof?
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
#define BLOCK_NUM 1000
bool clean[BLOCK_NUM] ; // clean bit for physical block; phy block ID -> bool
/*#
requires \exists integer j; 0 <= j < BLOCK_NUM/2 ==> clean[j] == true;
*/
void test(){
int i = 0;
/*#
loop assigns i;
loop invariant 0 <= i < BLOCK_NUM;
*/
while( i < BLOCK_NUM && clean[i] == false ){
i += 1;
}
}
I also tried a different version adding more information to the invariant
void test(){
int i = 0;
/*#
loop assigns i;
loop invariant 0 <= i < BLOCK_NUM &&
\exists integer j; i <= j < BLOCK_NUM/2 ==> clean[j] == true;
*/
while( i < BLOCK_NUM && clean[i] == false ){
i += 1;
}
//# assert 0 <= i < BLOCK_NUM;
}
This translates to the following proof condition.
Goal Invariant (preserved):
Assume {
Type: is_sint32(i#L4) /\ is_sint32(1 + i#L4).
[...]
Stmt { L4: }
(* Invariant *)
Have: (0 <= i#L4) /\ (i#L4 <= 999) /\
(((i#L4 <= i_3) -> ((i_3 <= 499) -> (clean#L1[i_3] = 1)))).
(* Then *)
Have: clean#L1[i#L4] = 0.
}
Prove: ((-1) <= i#L4) /\ (i#L4 <= 998) /\
(exists i_4 : Z. ((i#L4 < i_4) -> ((i_4 <= 499) -> (clean#L1[i_4] = 1)))).
--------------------------------------------------------------------------------
Prover Alt-Ergo 2.4.1: Timeout (Qed:101ms) (10s) (cached).
But still all solvers (including Z3 and CVC4) fail to show it.
There are two issues in your specification. First, your precondition is incorrect. What you want to say is that there exists a j such that j is between appropriate bounds and clean[j] == true, i.e.
requires \exists integer j; 0 <= j < BLOCK_NUM/2 && clean[j] == true;
More generally, and this can indeed be quite confusing at first, while a universal quantification generally goes along the lines of \forall x; P(x) ==> Q(x) (For any x such that P(x) holds, then Q(x) holds as well), existential quantification is usually something like \exists x, P(x) && Q(x): if you use implication instead of conjunction in that case, then any x that does not satisfy P will be a witness for the existential formula.
The second point is that your loop invariant is too weak: you don't say anything about the values you've examined so far: with just the bounds on i, it is impossible to deduce that we will eventually check the value of the cell containing true. By adding as an invariant that we have examined all the cells before i (and found false, since otherwise we wouldn't be in the loop anymore), all the proofs can be completed automatically:
loop invariant all_false: \forall integer j; 0 <= j < i ==> clean[j] == false;
Note that I've also added a name all_false to the new loop invariant. As your number of clauses grows, it is useful to give them explicit names, that will be used in Frama-C's output, so that you can quickly spot which proof obligation failed.
For what it's worth, here is the complete file (I removed the unnecessary includes)
#include <stdbool.h>
#define BLOCK_NUM 1000
bool clean[BLOCK_NUM] ; // clean bit for physical block; phy block ID -> bool
/*#
requires \exists integer j; 0 <= j < BLOCK_NUM/2 && clean[j] == true;
*/
void test(){
int i = 0;
/*#
loop assigns i;
loop invariant all_false: \forall integer j; 0 <= j < i ==> clean[j] == false;
loop invariant 0 <= i < BLOCK_NUM;
*/
while( i < BLOCK_NUM && clean[i] == false ){
i += 1;
}
}

Dafny - Violating Modifies when Calling in Loop

The following is based off of Secure Foundations's dafny implementation of a Dynamic Array.
I'm trying to create a test method that when calling push_back, invokes extend_buffer. This requires a prefix: calling push_back enough times to fill it up so the next time it's called, the buffer is extended. With a default size of 16, the prefix would consist of calling push_back 15 times. I find that if I make the call 15 times it verifies, but if I instead try to call in a for loop, I get the error call may violate context's modifies clause.
class Vector<T> {
static const DEFAULT_SIZE := 16
var buffer : array<T>
var capacity : int
var size : int
predicate Valid()
reads this, buffer
{
capacity >= DEFAULT_SIZE
&& capacity as int == buffer.Length
&& 0 <= size < capacity
}
method extend_buffer(value: T)
requires Valid()
ensures Valid()
ensures fresh(buffer)
ensures size as int < capacity as int - 1
ensures forall i : int :: 0 <= i < old(size) ==> buffer[i] == old(buffer[i])
ensures size == old(size)
ensures capacity == old(capacity) as int * 2
ensures buffer.Length == old(buffer.Length) * 2
modifies this`capacity, this`buffer
{
var old_buffer := this.buffer;
var old_size := this.capacity;
capacity := old_size * 2;
buffer := newArrayFill(capacity, value);
var i:= 0;
while i < old_size
invariant Valid();
invariant capacity > old_size;
invariant i < capacity;
invariant i < old_size;
invariant fresh(buffer)
invariant size < capacity - 1;
invariant size == old(size)
invariant capacity == old(capacity) * 2
invariant forall k : int :: 0 <= k < i ==> buffer[k] == old_buffer[k] == old(buffer[k])
{
buffer[i] := old_buffer[i];
if i == old_size - 1 {
break;
}
i := i + 1;
}
}
method push_back(value:T)
requires Valid()
ensures Valid();
ensures old(size as int) < buffer.Length
ensures buffer[old(size)] == value
ensures size == old(size) + 1
ensures if old(size) + 1 == old(capacity) then fresh(buffer) else buffer == old(buffer)
ensures forall i : int :: 0 <= i < old(size) ==> buffer[i] == old(buffer[i])
ensures forall i : int :: size <= i < old(buffer.Length) ==> buffer[i] == old(buffer[i])
ensures if size == old(capacity) then fresh(buffer) else !fresh(buffer) && buffer == old(buffer)
modifies this, this.buffer, this`size
{
if (size + 1 == capacity)
{
extend_buffer(value);
}
buffer[size] := value;
size := size + 1;
}
method {:extern "Extern", "newArrayFill"} newArrayFill<T>(n: int, t: T) returns (ar: array<T>)
ensures ar.Length == n as int
ensures forall i | 0 <= i < n :: ar[i] == t
ensures fresh(ar)
constructor(default_val:T)
ensures Valid()
ensures fresh(buffer)
ensures size == 0
ensures capacity == DEFAULT_SIZE
ensures capacity as int == buffer.Length
{
size := 0;
capacity := DEFAULT_SIZE;
new;
buffer := newArrayFill(DEFAULT_SIZE, default_val);
}
}
method push_back_should_extend()
{
var arr := new Vector(0);
label L:
var oracleValue := 7;
for i : int := 0 to arr.capacity - 1
invariant arr.Valid()
{
arr.push_back(oracleValue);
}
}
I'm assuming the issue arises from push_back claiming it modifies this, which I feel is overstating what it actually modifies, but when I remove this, the error just moves to the call to extend_buffer.
Interestingly, adding in the following twostate:
twostate predicate sameBuffer(v: Vector)
reads v`buffer
{
v.buffer == old(v.buffer)
}
and then adding invariant sameBuffer#L(arr) to push_back_should_extend's for loop also doesn't verify, even though it's clear from ensures if old(size) + 1 == old(capacity) then fresh(buffer) else !fresh(buffer) && buffer == old(buffer) that the memory for buffer doesn't change throughout the prefix.
I feel this could be addressed by an inductive lemma, but while I understand the form of lemma's, I lack the ability to apply and derive them.
Adding
invariant fresh(arr.buffer)
to the for loop seems to fix it. Does that do what you want?

Verification of function that sums array of int vs array of float using FramaC/WP

I am trying to specify and verify this simple function that sums up an array.
This specification for an array of ints is accepted by WP:
/*# axiomatic Sum_Int {
logic int sum_int(int *values, integer begin, integer end)
reads values[begin .. (end - 1)];
axiom empty_int:
\forall int *p, integer begin, end; begin >= end
==> sum_int(p, begin, end) == 0;
axiom range_int:
\forall int *p, integer begin, end; begin < end
==> sum_int(p, begin, end) == sum_int(p, begin, end - 1) + p[end - 1];
}
*/
/*# requires \valid_read(values + (0 .. size - 1));
assigns \nothing;
ensures \result == sum_int(values, 0, size);
*/
int sum_int_array(const int *values, size_t size) {
int sum = 0;
/*# loop invariant 0 <= index <= size;
loop invariant sum == sum_int(values, 0, index);
loop assigns sum, index;
loop variant size - index;
*/
for (size_t index = 0; index < size; index++) {
sum += values[index];
}
return sum;
}
But the same specification for an array of floats cannot be proven by WP:
/*# axiomatic Sum_Real {
logic float sum_real(float *values, integer begin, integer end)
reads values[begin .. (end - 1)];
axiom empty_real:
\forall float *p, integer begin, end; begin >= end
==> sum_real(p, begin, end) == 0;
axiom range_real:
\forall float *p, integer begin, end; begin < end
==> sum_real(p, begin, end) == sum_real(p, begin, end - 1) + p[end - 1];
}
*/
/*# requires \valid_read(values + (0 .. size - 1));
requires \forall integer i; 0 <= i < size ==> \is_finite(values[i]);
assigns \nothing;
ensures \result == sum_real(values, 0, size);
*/
float sum_float_array(const float *values, size_t size) {
float sum = 0;
/*# loop invariant 0 <= index <= size;
loop invariant sum == sum_real(values, 0, index);
loop assigns sum, index;
loop variant size - index;
*/
for (size_t index = 0; index < size; index++) {
sum += values[index];
}
return sum;
}
The axiomatic definition is the same (and I think it should be the same, if it is correct). The only difference is the added precondition for the float array:
requires \forall integer i; 0 <= i < size ==> \is_finite(values[i]);
Which I am not sure if it is necessary.
What information is missing here for WP to be able to verify the float version?

Frama-c verification (From decimal to binary function)

i'm trying to verify this in frama-c. I don't understand why the post-condition can't be verified. Can someone help me , please?
I initially made ​​an axiomatic with a base case and an inductive case. The problem is that the post-condition is not verified. All the loop invariant are true.
#include <stdio.h>
/*# axiomatic ToBinary {
logic integer tobinary{L}(integer n)
reads n;
axiom binary1{L} :
\forall integer n;
(n == 0) ==> tobinary(n) == 0;
axiom binary2{L} :
\forall integer n;
(n > 0 ) ==> tobinary(n) == n%2 + 10*tobinary(n/2);
}
*/
/*#
requires n >= 0;
assigns \nothing;
ensures \result == tobinary(n);
*/
int decimal_binary(int n) /* Function to convert decimal to binary.*/
{
int rem, i=1, binary=0;
/*#
loop assigns n,rem,i,binary;
loop invariant 1 <= i <= i*10 ;
loop invariant 0 <= binary;
*/
while (n!=0)
{
rem=n%2;
n/=2;
binary+=rem*i;
i*=10;
}
return binary;
}

Resources