While reviewing a codebase, I came upon a particular piece of code that triggered a warning regarding an "out of bounds access". After looking at the code, I could not see a way for the reported access to happen - and tried to minimize the code to create a reproducible example. I then checked this example with two commercial static analysers that I have access to - and also with the open-source Frama-C.
All 3 of them see the same "out of bounds" access.
I don't. Let's have a look:
3 extern int checker(int id);
4 extern int checker2(int id);
5
6 int compute(int *q)
7 {
8 int res = 0, status;
9
10 status = checker2(12);
11 if (!status) {
12 status = 1;
13 *q = 2;
14 for(int i=0; i<2 && 0!=status; i++) {
15 if (checker(i)) {
16 res = i;
17 status=checker2(i);
18 }
19 }
20 }
21 if (!status)
22 *q = res;
23 return status;
24 }
25
26 int someFunc(int id)
27 {
28 int p;
29 extern int data[2];
30
31 int status = checker2(132);
32 status |= compute(&p);
33 if (status == 0) {
34 return data[p];
35 } else
36 return -1;
37 }
Please don't try to judge the quality of the code, or why it does things the way it does. This is a hacked, cropped and mutated version of the original, with the sole intent being to reach a small example that demonstrates the issue.
All analysers I have access to report the same thing - that the indexing in the caller at line 34, doing the return data[p]
may read via the invalid index "2". Here's the output from Frama-C - but note that two commercial static analysers provide exactly the same assessment:
$ frama-c -val -main someFunc -rte why.c |& grep warning
...
why.c:34:[value] warning: accessing out of bounds index. assert p < 2;
Let's step the code in reverse, to see how this out of bounds access at line 34 can happen:
status
from both calls to checker2
and compute
should be 0.compute
to return 0 (at line 32 in the caller, line 23 in the callee), it means that we have performed the assignment at line 22 - since it is guarded at line 21 with a check for status
being 0. So we wrote in the passed-in pointer q
, whatever was stored in variable res
. This pointer points to the variable used to perform the indexing - the supposed out-of-bounds index.data
, which is dimensioned to contain exactly two elements, we must have written a value that is neither 0 nor 1 into res
.res
via the for
loop at 14; which will conditionally assign into res
; if it does assign, the value it will write will be one of the two valid indexes 0 or 1 - because those are the values that the for
loop allows to go through (it is bound with i<2
).status
at line 12, if we do reach line 12, we will for sure enter the loop at least once. And if we do write into res
, we will write a nice valid index.status
checks - at either line 11 or at line 21 fail, we will return with a non-zero status
; so whatever value we wrote (or didn't, and left uninitialised) into the passed-in q
is irrelevant; the caller will not read that value, due to the check at line 33.So either I am missing something and there is indeed a scenario that leads to an out of bounds access with index 2 at line 34 (how?) or this is an example of the limits of mainstream formal verification.
Help?
== 0
and != 0
inside a range, such as [INT_MIN; INT_MAX]
, you need to tell Frama-C/Eva to split the cases.By adding //@ split
annotations in the appropriate spots, you can tell Frama-C/Eva to maintain separate states, thus preventing merging them before status
is evaluated.
Here's how your code would look like, in this case (courtesy of @Virgile):
extern int checker(int id);
extern int checker2(int id);
int compute(int *q)
{
int res = 0, status;
status = checker2(12);
//@ split status <= 0;
//@ split status == 0;
if (!status) {
status = 1;
*q = 2;
for(int i=0; i<2 && 0!=status; i++) {
if (checker(i)) {
res = i;
status=checker2(i);
}
}
}
//@ split status <= 0;
//@ split status == 0;
if (!status)
*q = res;
return status;
}
int someFunc(int id)
{
int p;
extern int data[2];
int status = checker2(132);
//@ split status <= 0;
//@ split status == 0;
status |= compute(&p);
if (status == 0) {
return data[p];
} else
return -1;
}
In each case, the first split
annotation tells Eva to consider the cases status <= 0
and status > 0
separately; this allows "breaking" the interval [INT_MIN, INT_MAX]
into [INT_MIN, 0]
and [1, INT_MAX]
; the second annotation allows separating [INT_MIN, 0]
into [INT_MIN, -1]
and [0, 0]
. When these 3 states are propagated separately, Eva is able to precisely distinguish between the different situations in the code and avoid the spurious alarm.
You also need to allow Frama-C/Eva some margin for keeping the states separated (by default, Eva will optimize for efficiency, merging states somewhat aggressively); this is done by adding -eva-precision 1
(higher values may be required for your original scenario).
-eva-domains sign
(previously -eva-sign-domain
) and -eva-partition-history N
Frama-C/Eva also has other options which are related to splitting states; one of them is the signs domain, which computes information about sign of variables, and is useful to distinguish between 0 and non-zero values. In some cases (such as a slightly simplified version of your code, where status |= compute(&p);
is replaced with status = compute(&p);
), the sign domain may help splitting without the need for annotations. Enable it using -eva-domains sign
(-eva-sign-domain
for Frama-C <= 20).
Another related option is -eva-partition history N
, which tells Frama-C to keep the states partitioned for longer.
Note that keeping states separated is a bit costly in terms of analysis, so it may not scale when applied to the "real" code, if it contains several more branches. Increasing the values given to -eva-precision
and -eva-partition-history
may help, as well as adding @ split
annotations.
I'd like to add some remarks which will hopefully be useful in the future:
Frama-C contains several plug-ins and analyses. Here in particular, you are using the Eva plug-in. It performs an analysis based on abstract interpretation that reports all possible runtime errors (undefined behaviors, as the C standard puts it) in a program. Using -rte
is thus unnecessary, and adds noise to the result. If Eva cannot be certain about the absence of some alarm, it will report it.
Replace the -val
option with -eva
. It's the same thing, but the former is deprecated.
If you want to improve precision (to remove false alarms), add -eva-precision N
, where 0 <= N <= 11
. In your example program, it doesn't change much, but in complex programs with multiple callstacks, extra precision will take longer but minimize the number of false alarms.
Also, consider providing a minimal specification for the external functions, to avoid warnings; here they contain no pointers, but if they did, you'd need to provide an assigns clause to explicitly tell Frama-C whether the functions modify such pointers (or any global variables, for instance).
With the Frama-C graphical interface and the Studia plug-in (accessible by right-clicking an expression of interest and choosing the popup menu Studia -> Writes), and using the Values panel in the GUI, you can easily track what the analysis inferred, and better understand where the alarms and values come from. The only downside is that, it does not report exactly where merges happen. For the most precise results possible, you may need to add calls to an Eva built-in, Frama_C_show_each(exp)
, and put it inside a loop to get Eva to display, at each iteration of its analysis, the values contained in exp
.
See section 9.3 (Displaying intermediate results) of the Eva user manual for more details, including similar built-ins (such as Frama_C_domain_show_each
and Frama_C_dump_each
, which show information about abstract domains). You may need to #include "__fc_builtin.h"
in your program. You can use #ifdef __FRAMAC__
to allow the original code to compile when including this Frama-C-specific file.
Frama-C is a semantic-based tool whose main analyses are exhaustive, but may contain false positives: Frama-C may report alarms when they do not happen, but it should never forget any possible alarm. It's a trade-off, you can't have an exact tool in all cases (though, in this example, with sufficient -eva-precision, Frama-C is exact, as in reporting only issues which may actually happen).
In this sense, erroneous would mean that Frama-C "forgot" to indicate some issue, and we'd be really concerned about it. Indicating an alarm where it may not happen is still problematic for the user (and we work to improve it, so such situations should happen less often), but not a bug in Frama-C, and so we prefer using the term imprecisely, e.g. "Frama-C/Eva imprecisely reports an out of bounds access".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With