Skip to content

Commit a1a7b6c

Browse files
committed
Symex: restore guards on function exit
Executing a function may have a cumulative effect on the state guard. For example, if the callee contained ASSUME statements that rendered one or more control-flow options unviable then the guard might still embody that restriction (i.e. for if(x) ASSUME(false) the guard might still be `!x`). However, on function return we know that all control-flow paths have converged or been shown to be unviable, so we can restore the simpler guard as it was when we entered the callee function. Exceptions: (a) if the guard is false it would be correct but inefficient to restore it; keep it false until we find a convergeance with another viable path (b) if we're doing path-sensitive symex then we do tail duplication, and there are no control-flow convergeances. Keep the guard as-was. (c) if we're executing a multi-threaded program then symex_assume_l2 uses the guard to accumulate assumptions, which we must not discard. In truth this optimisation is applicable whenever a block postdominates another, but function structure gives us a cheap way to establish postdominance without analysis (in the absence of setjmp/longjmp at least)
1 parent ca13458 commit a1a7b6c

File tree

16 files changed

+202
-9
lines changed

16 files changed

+202
-9
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
void g() {
2+
g();
3+
}
4+
5+
void f(int y) {
6+
7+
if(y == 5) {
8+
g();
9+
y = 10;
10+
}
11+
12+
}
13+
14+
int main(int argc, char **argv) {
15+
16+
f(argc);
17+
assert(argc == 1);
18+
19+
}
20+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CORE
2+
test.c
3+
--show-vcc --unwind 10
4+
^\{1\} main::argc!0@1#1 = 1$
5+
^EXIT=0$
6+
^SIGNAL=0$
7+
--
8+
^\{-[0-9]+\} f::y!0@1#[0-9]+ = 10$
9+
--
10+
This checks that the assertion, argc == 1, is unguarded (i.e. is not of the form
11+
'guard => condition'). Such a guard is redundant, but could be added before goto-symex
12+
made sure to restore guards to their state at function entry.
13+
We also check that no VCC is created for the unreachable code 'y = 10;'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CORE
2+
test.c
3+
--unwind 10
4+
^\[main.assertion.[0-9]+\] line [0-9]+ assertion argc == 1: FAILURE$
5+
^VERIFICATION FAILED$
6+
^EXIT=10$
7+
^SIGNAL=0$
8+
--
9+
--
10+
The main test is in test.desc; this just checks that the test is executable and
11+
the assertion fails as expected.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
void f(int y) {
2+
3+
if(y == 5) {
4+
for(int i = 0; i < 20; ++i) { }
5+
y = 10;
6+
}
7+
8+
}
9+
10+
int main(int argc, char **argv) {
11+
12+
f(argc);
13+
assert(argc == 1);
14+
15+
}
16+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CORE
2+
test.c
3+
--show-vcc --unwind 10
4+
^\{1\} main::argc!0@1#1 = 1$
5+
^EXIT=0$
6+
^SIGNAL=0$
7+
--
8+
^\{-[0-9]+\} f::y!0@1#[0-9]+ = 10$
9+
--
10+
This checks that the assertion, argc == 1, is unguarded (i.e. is not of the form
11+
'guard => condition'). Such a guard is redundant, but could be added before goto-symex
12+
made sure to restore guards to their state at function entry.
13+
We also check that no VCC is created for the unreachable code 'y = 10;'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CORE
2+
test.c
3+
--unwind 10
4+
^\[main.assertion.[0-9]+\] line [0-9]+ assertion argc == 1: FAILURE$
5+
^VERIFICATION FAILED$
6+
^EXIT=10$
7+
^SIGNAL=0$
8+
--
9+
--
10+
The main test is in test.desc; this just checks that the test is executable and
11+
the assertion fails as expected.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
void g(int x) {
2+
return g(x + 1);
3+
}
4+
5+
void f(int y) {
6+
7+
if(y == 5) {
8+
g(1);
9+
y = 10;
10+
}
11+
12+
}
13+
14+
int main(int argc, char **argv) {
15+
16+
f(argc);
17+
assert(argc == 1);
18+
19+
}
20+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CORE
2+
test.c
3+
--show-vcc --depth 1000
4+
^\{1\} main::argc!0@1#1 = 1$
5+
^EXIT=0$
6+
^SIGNAL=0$
7+
--
8+
^\{-[0-9]+\} f::y!0@1#[0-9]+ = 10$
9+
--
10+
This checks that the assertion, argc == 1, is unguarded (i.e. is not of the form
11+
'guard => condition'). Such a guard is redundant, but could be added before goto-symex
12+
made sure to restore guards to their state at function entry.
13+
We also check that no VCC is created for the unreachable code 'y = 10;'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CORE
2+
test.c
3+
--depth 1000
4+
^\[main.assertion.[0-9]+\] line [0-9]+ assertion argc == 1: FAILURE$
5+
^VERIFICATION FAILED$
6+
^EXIT=10$
7+
^SIGNAL=0$
8+
--
9+
--
10+
The main test is in test.desc; this just checks that the test is executable and
11+
the assertion fails as expected.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
void f(int y) {
2+
3+
if(y == 5) {
4+
__CPROVER_assume(0);
5+
y = 10;
6+
}
7+
8+
}
9+
10+
int main(int argc, char **argv) {
11+
12+
f(argc);
13+
assert(argc == 1);
14+
15+
}
16+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CORE
2+
test.c
3+
--show-vcc
4+
^\{1\} main::argc!0@1#1 = 1$
5+
^EXIT=0$
6+
^SIGNAL=0$
7+
--
8+
^\{-[0-9]+\} f::y!0@1#[0-9]+ = 10$
9+
--
10+
This checks that the assertion, argc == 1, is unguarded (i.e. is not of the form
11+
'guard => condition'). Such a guard is redundant, but could be added before goto-symex
12+
made sure to restore guards to their state at function entry.
13+
We also check that no VCC is created for the unreachable code 'y = 10;'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CORE
2+
test.c
3+
4+
^\[main.assertion.[0-9]+\] line [0-9]+ assertion argc == 1: FAILURE$
5+
^VERIFICATION FAILED$
6+
^EXIT=10$
7+
^SIGNAL=0$
8+
--
9+
--
10+
The main test is in test.desc; this just checks that the test is executable and
11+
the assertion fails as expected.

src/goto-symex/call_stack.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ class call_stackt : public std::vector<framet>
2626
return back();
2727
}
2828

29-
framet &new_frame(symex_targett::sourcet calling_location)
29+
framet &new_frame(
30+
symex_targett::sourcet calling_location, const guardt &guard)
3031
{
31-
emplace_back(calling_location);
32+
emplace_back(calling_location, guard);
3233
return back();
3334
}
3435

src/goto-symex/frame.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ struct framet
2727
std::map<goto_programt::const_targett, goto_state_listt> goto_state_map;
2828
symex_targett::sourcet calling_location;
2929
std::vector<irep_idt> parameter_names;
30-
30+
guardt guard_at_function_start;
3131
goto_programt::const_targett end_of_function;
3232
exprt return_value = nil_exprt();
3333
bool hidden_function = false;
@@ -48,8 +48,9 @@ struct framet
4848

4949
std::unordered_map<irep_idt, loop_infot> loop_iterations;
5050

51-
explicit framet(symex_targett::sourcet _calling_location)
52-
: calling_location(std::move(_calling_location))
51+
framet(symex_targett::sourcet _calling_location, const guardt &state_guard)
52+
: calling_location(std::move(_calling_location)),
53+
guard_at_function_start(state_guard)
5354
{
5455
}
5556
};

src/goto-symex/goto_symex_state.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ goto_symex_statet::goto_symex_statet(
4444
fresh_l2_name_provider(fresh_l2_name_provider)
4545
{
4646
threads.emplace_back(guard_manager);
47-
call_stack().new_frame(source);
47+
call_stack().new_frame(source, guardt(true_exprt(), manager));
4848
}
4949

5050
goto_symex_statet::~goto_symex_statet()=default;

src/goto-symex/symex_function_call.cpp

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ void goto_symext::symex_function_call_code(
302302

303303
// produce a new frame
304304
PRECONDITION(!state.call_stack().empty());
305-
framet &frame = state.call_stack().new_frame(state.source);
305+
framet &frame = state.call_stack().new_frame(state.source, state.guard);
306306

307307
// preserve locality of local variables
308308
locality(identifier, state, path_storage, goto_function, ns);
@@ -332,7 +332,10 @@ void goto_symext::symex_function_call_code(
332332

333333
/// pop one call frame
334334
static void
335-
pop_frame(goto_symext::statet &state, const path_storaget &path_storage)
335+
pop_frame(
336+
goto_symext::statet &state,
337+
const path_storaget &path_storage,
338+
bool doing_path_exploration)
336339
{
337340
PRECONDITION(!state.call_stack().empty());
338341

@@ -346,6 +349,26 @@ pop_frame(goto_symext::statet &state, const path_storaget &path_storage)
346349
// restore L1 renaming
347350
state.level1.restore_from(frame.old_level1);
348351

352+
// If the program is multi-threaded then the state guard is used to
353+
// accumulate assumptions (in symex_assume_l2) and must be left alone.
354+
// If however it is single-threaded then we should restore the guard, as the
355+
// guard coming out of the function may be more complex (e.g. if the callee
356+
// was { if(x) __CPROVER_assume(false); } then the guard may still be `!x`),
357+
// but at this point all control-flow paths have either converged or been
358+
// proven unviable, so we can stop specifying the callee's constraints when
359+
// we generate an assumption or VCC.
360+
361+
// If the guard is false, *this* path is unviable and we shouldn't discard
362+
// that knowledge. If we're doing path exploration then we do
363+
// tail-duplication, and we actually *are* in a more-restricted context
364+
// than we were when the function began.
365+
if(state.threads.size() == 1 &&
366+
!state.guard.is_false() &&
367+
!doing_path_exploration)
368+
{
369+
state.guard = frame.guard_at_function_start;
370+
}
371+
349372
symex_renaming_levelt::viewt view;
350373
state.get_level2().current_names.get_view(view);
351374

@@ -386,7 +409,7 @@ void goto_symext::symex_end_of_function(statet &state)
386409
state.guard.as_expr(), state.source.function_id, state.source, hidden);
387410

388411
// then get rid of the frame
389-
pop_frame(state, path_storage);
412+
pop_frame(state, path_storage, symex_config.doing_path_exploration);
390413
}
391414

392415
/// Preserves locality of parameters of a given function by applying L1

0 commit comments

Comments
 (0)