Skip to content

Commit 8534a11

Browse files
Add function pointer restriction option
This adds a new option, --restrict-function-pointer, to cbmc. This lets a user specify a list of possible pointer targets for specific function pointer call sites, rather than have remove_function_pointers guess possible values. The intended purpose behind this is to prevent excessive symex time wasted on exploring paths the user knows the program can never actually take.
1 parent 7a70b13 commit 8534a11

File tree

25 files changed

+1035
-1
lines changed

25 files changed

+1035
-1
lines changed

doc/cprover-manual/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
[A Short Tutorial](cbmc/tutorial/),
1010
[Loop Unwinding](cbmc/unwinding/),
1111
[Assertion Checking](cbmc/assertions/),
12+
[Restricting function pointers](cbmc/restrict-function-pointer/),
1213
[Memory Analyzer](cbmc/memory-analyzer/),
1314
[Program Harness](cbmc/goto-harness/)
1415

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
[CPROVER Manual TOC](../../)
2+
3+
## Restricting function pointers
4+
5+
### Motivation
6+
7+
CBMC comes with a way to resolve calls to function pointers to direct function
8+
calls. This is needed because symbolic execution itself can't handle calls to
9+
function pointers. In practice, this looks something like this:
10+
11+
Given that there are functions with these signatures available in the program:
12+
13+
```
14+
int f(int x);
15+
int g(int x);
16+
int h(int x);
17+
```
18+
19+
And we have a call site like this:
20+
21+
```
22+
typedef int(*fptr_t)(int x);
23+
void call(fptr_t fptr) {
24+
int r = fptr(10);
25+
assert(r > 0);
26+
}
27+
```
28+
29+
Function pointer removal will turn this into code similar to this:
30+
31+
```
32+
void call(fptr_t fptr) {
33+
int r;
34+
if(fptr == &f) {
35+
r = f(10);
36+
} else if(fptr == &g) {
37+
r = g(10);
38+
} else if(fptr == &h) {
39+
r = h(10);
40+
} else {
41+
// sanity check
42+
assert(false);
43+
assume(false);
44+
}
45+
return r;
46+
}
47+
```
48+
49+
This works well enough for simple cases. However, this is a "dumb" replacement
50+
only based on the signature of the function, so if there are many functions
51+
matching a particular signature, or if some of these functions are expensive in
52+
symex (e.g. functions with lots of loops or recursion), then this can be a bit
53+
cumbersome - especially if we, as a user, already know that a particular
54+
function pointer will only resolve to a single function or a small set of
55+
functions. This is what the `--restrict-function-pointer` option allows.
56+
57+
### Example
58+
59+
Take the motivating example. Let us assume that we know for a fact that `call`
60+
will always receive pointers to either `f` or `g` during actual executions of
61+
the program, and symbolic execution for `h` is too expensive to simply ignore
62+
the cost of its branch. For this, we will label the places in each function
63+
where function pointers are being called, to this pattern:
64+
65+
```
66+
<function-name>.function_pointer_call.<N>
67+
```
68+
69+
where `N` is referring to which function call it is - so the first call to a
70+
function pointer in a function will have `N=1`, the 5th `N=5` etc.
71+
72+
We can call `cbmc` with `--restrict-function-pointer
73+
call.function_pointer_call.1/f,g`. This can be read as
74+
75+
> For the first call to a function pointer in the function `call`, assume that
76+
> it can only be a call to `f` or `g`
77+
78+
The resulting code looks similar to the original example, except now there will
79+
not be a call to `h`:
80+
81+
```
82+
void call(fptr_t fptr) {
83+
int r;
84+
if(fptr == &f) {
85+
r = f(10);
86+
} else if(fptr == &g) {
87+
r = g(10);
88+
} else {
89+
// sanity check
90+
assert(false);
91+
assume(false);
92+
}
93+
return r;
94+
}
95+
```
96+
97+
Another example: Imagine we have a simple virtual filesystem API and implementation
98+
like this:
99+
100+
```
101+
typedef struct filesystem_t filesystem_t;
102+
struct filesystem_t {
103+
int (*open)(filesystem_t *filesystem, const char* file_name);
104+
};
105+
106+
int fs_open(filesystem_t *filesystem, const char* file_name) {
107+
filesystem->open(filesystem, file_name);
108+
}
109+
110+
int nullfs_open(filesystem_t *filesystem, const char* file_name) {
111+
return -1;
112+
}
113+
114+
filesystem_t nullfs_val = {.open = nullfs_open};
115+
filesystem *const nullfs = &nullfs_val;
116+
117+
filesystem_t *get_fs_impl() {
118+
// some fancy logic to determine
119+
// which filesystem we're getting -
120+
// in-memory, backed by a database, OS file system
121+
// - but in our case, we know that
122+
// it always ends up being nullfs
123+
// for the cases we care about
124+
return nullfs;
125+
}
126+
int main(void) {
127+
filesystem_t *fs = get_fs_impl();
128+
assert(fs_open(fs, "hello.txt") != -1);
129+
}
130+
```
131+
132+
In this case, the assumption is that *we* know that in our `main`, `fs` can be
133+
nothing other than `nullfs`; But perhaps due to the logic being too complicated
134+
symex ends up being unable to figure this out, so in the call to `fs_open()` we
135+
end up branching on all functions matching the signature of
136+
`filesystem_t::open`, which could be quite a few functions within the program.
137+
Worst of all, if it's address is ever taken in the program, as far as the "dumb"
138+
function pointer removal is concerned it could be `fs_open()` itself due to it
139+
having a matching signature, leading to symex being forced to follow a
140+
potentially infinite recursion until its unwind limit.
141+
142+
In this case we can again restrict the function pointer to the value which we
143+
know it will have:
144+
145+
```
146+
--restrict-function-pointer fs_open.function_pointer_call.1/nullfs_open
147+
```
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#include <assert.h>
2+
3+
typedef int (*fptr_t)(int);
4+
5+
fptr_t get_f(void);
6+
fptr_t get_g(void);
7+
8+
void use_fg(int choice, fptr_t fptr, fptr_t gptr)
9+
{
10+
assert((choice ? fptr : gptr)(10) == 10 + choice);
11+
}
12+
13+
// this is just here to distinguish the behavior from FP removal, which'd include g
14+
int g_always_false_cond = 0;
15+
16+
int main(void)
17+
{
18+
g_always_false_cond = 1;
19+
use_fg(0, get_f(), get_g());
20+
use_fg(1, get_f(), get_g());
21+
}
22+
23+
int f(int x)
24+
{
25+
return x + 1;
26+
}
27+
28+
int g(int x)
29+
{
30+
return x;
31+
}
32+
33+
int h(int x)
34+
{
35+
return x / 2;
36+
}
37+
38+
fptr_t get_f(void)
39+
{
40+
if(!g_always_false_cond)
41+
{
42+
return f;
43+
}
44+
else
45+
{
46+
return h;
47+
}
48+
}
49+
50+
fptr_t get_g(void)
51+
{
52+
if(!g_always_false_cond)
53+
{
54+
return g;
55+
}
56+
else
57+
{
58+
return h;
59+
}
60+
}

regression/cbmc/restrict-function-pointer-to-complex-expression/test.desc

Whitespace-only changes.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#include <assert.h>
2+
3+
typedef int (*fptr_t)(int);
4+
5+
fptr_t get_f(void);
6+
7+
void use_f(fptr_t fptr)
8+
{
9+
assert(fptr(10) >= 10);
10+
}
11+
12+
void select_f(void);
13+
void select_g(void);
14+
void select_h(void);
15+
16+
int main(void)
17+
{
18+
select_f();
19+
use_f(get_f());
20+
select_g();
21+
use_f(get_f());
22+
}
23+
24+
int f(int x)
25+
{
26+
return x + 1;
27+
}
28+
int g(int x)
29+
{
30+
return x;
31+
}
32+
int h(int x)
33+
{
34+
return x - 1;
35+
}
36+
37+
int g_select_function = 0;
38+
39+
void select_f(void)
40+
{
41+
g_select_function = 0;
42+
}
43+
void select_g(void)
44+
{
45+
g_select_function = 1;
46+
}
47+
void select_h(void)
48+
{
49+
g_select_function = 2;
50+
}
51+
52+
fptr_t get_f(void)
53+
{
54+
if(g_select_function == 0)
55+
{
56+
return f;
57+
}
58+
else if(g_select_function == 1)
59+
{
60+
return g;
61+
}
62+
else
63+
{
64+
return h;
65+
}
66+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
CORE
2+
test.c
3+
--restrict-function-pointer use_f.function_pointer_call.1/f,g --show-goto-functions
4+
f\(10\)
5+
g\(10\)
6+
^EXIT=0$
7+
^SIGNAL=0$
8+
--
9+
h\(10\)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#include <assert.h>
2+
3+
typedef int (*fptr_t)(int);
4+
5+
fptr_t get_f(void);
6+
7+
void use_f(fptr_t fptr)
8+
{
9+
assert(fptr(10) >= 10);
10+
}
11+
12+
void select_f(void);
13+
void select_g(void);
14+
void select_h(void);
15+
16+
int main(void)
17+
{
18+
select_f();
19+
use_f(get_f());
20+
select_g();
21+
use_f(get_f());
22+
select_h();
23+
use_f(get_f());
24+
}
25+
26+
int f(int x)
27+
{
28+
return x + 1;
29+
}
30+
int g(int x)
31+
{
32+
return x;
33+
}
34+
int h(int x)
35+
{
36+
return x - 1;
37+
}
38+
39+
int g_select_function = 0;
40+
41+
void select_f(void)
42+
{
43+
g_select_function = 0;
44+
}
45+
void select_g(void)
46+
{
47+
g_select_function = 1;
48+
}
49+
void select_h(void)
50+
{
51+
g_select_function = 2;
52+
}
53+
54+
fptr_t get_f(void)
55+
{
56+
if(g_select_function == 0)
57+
{
58+
return f;
59+
}
60+
else if(g_select_function == 1)
61+
{
62+
return g;
63+
}
64+
else
65+
{
66+
return h;
67+
}
68+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CORE
2+
test.c
3+
--restrict-function-pointer use_f.function_pointer_call.1/f,g
4+
\[use_f\.assertion\.1\] line \d+ use_f.function_pointer_call.1 must be one of \[f, g\]: FAILURE
5+
^EXIT=10$
6+
^SIGNAL=0$

0 commit comments

Comments
 (0)