Skip to content

Commit 4c0bc64

Browse files
committed
Symex: distinguish reachability from the state guard
Previously the state guard being false was taken to indicate unreachable code, and different causes of unreachability were handled inconsistently: * ASSUME false implied unreachability but had no effect on the guard * --unwind or --depth being exceeded usually meant setting the guard false This meant that expensive code downstream of an ASSUME false *was* executed, but the state guard was maintained as usual including shrinking the guard when branches converge, while --unwind or --depth limit breaks led to code *not* being executed (vital for these options to function properly) but the state guard grew ever larger because the part that had gone missing (been set to false) never got merged back in. This commit distinguishes the two concepts: the "reachable" flag is set false whenever a limit break OR and ASSUME false happens, but state guard maintenance continues as normal. A new symex_unreachable_goto tries to find some way through the CFG back to reachable code; when it can't due to loops *then* it sets the state guard to false, signifying our not being able to figure out where the guard ought to be merged back in. This means that ASSUME false instructions now correctly truncate execution and are recognised as unreachable code for the purposes of state merging (leading to better constant propagation downstream), but in some cases we may see more guard growth because code downstream of an ASSUME false is executed less rigorously that before, which could result in the guard not being merged or being merged later than it otherwise would have been. Depth and unwind limit breaks behave roughly as before, except that there is some chance the state guard will be merged rather than discarded, reducing costs downstream.
1 parent bb09891 commit 4c0bc64

27 files changed

+345
-46
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CORE paths-lifo-expected-failure
2+
test.c
3+
--show-vcc
4+
^Generated 1 VCC\(s\), 1 remaining after simplification$
5+
^\{1\} false$
6+
^EXIT=0$
7+
^SIGNAL=0$
8+
--
9+
--
10+
Note: disabled for paths-lifo mode, which never merges state guards
11+
This checks that despite being unreachable due to an assume(false), the state guard
12+
is returned to TRUE by the time the assertion is executed.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
int main(int argc, char **argv)
2+
{
3+
if(argc == 1)
4+
{
5+
do
6+
{
7+
__CPROVER_assume(0);
8+
} while(argc < 2);
9+
}
10+
11+
assert(0);
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CORE
2+
test.c
3+
4+
^VERIFICATION FAILED$
5+
^EXIT=10$
6+
^SIGNAL=0$
7+
--
8+
--
9+
This tests that a __CPROVER_assume(0) prompts symex to assume that a conditional
10+
backward branch is not taken.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CORE paths-lifo-expected-failure
2+
test.c
3+
--show-vcc
4+
^Generated 1 VCC\(s\), 1 remaining after simplification$
5+
^\{1\} goto_symex::\\guard#1$
6+
^EXIT=0$
7+
^SIGNAL=0$
8+
--
9+
--
10+
Note: disabled for paths-lifo mode, which never merges state guards
11+
This checks that the guard from a loop containing an assume(false), and with
12+
an unconditional backedge, is discarded as expected, resulting in a non-trivial
13+
guard at the assertion.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
int main(int argc, char **argv)
2+
{
3+
if(argc == 1)
4+
{
5+
while(argc < 2)
6+
{
7+
__CPROVER_assume(0);
8+
}
9+
}
10+
11+
assert(0);
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
CORE
2+
test.c
3+
4+
^VERIFICATION FAILED$
5+
^EXIT=10$
6+
^SIGNAL=0$
7+
--
8+
--
9+
This tests that a __CPROVER_assume(0) in an infinite loop terminates the loop
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CORE paths-lifo-expected-failure
2+
test.c
3+
--show-vcc
4+
^Generated 1 VCC\(s\), 1 remaining after simplification$
5+
^\{1\} false$
6+
^EXIT=0$
7+
^SIGNAL=0$
8+
--
9+
--
10+
Note: disabled for paths-lifo mode, which never merges state guards
11+
This checks that an assume(false) prior to a loop correctly results
12+
in symex stepping around that loop, resulting in the state guard
13+
returning to true.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
int main(int argc, char **argv)
2+
{
3+
if(argc == 1)
4+
{
5+
__CPROVER_assume(0);
6+
while(1)
7+
{
8+
}
9+
}
10+
11+
assert(0);
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CORE
2+
test.c
3+
4+
^VERIFICATION FAILED$
5+
^EXIT=10$
6+
^SIGNAL=0$
7+
--
8+
--
9+
This tests that with a __CPROVER_assume(0) before an infinite loop,
10+
symex terminates without needing a `--unwind` setting.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CORE paths-lifo-expected-failure
2+
test.c
3+
--show-vcc
4+
^Generated 1 VCC\(s\), 1 remaining after simplification$
5+
^\{1\} false$
6+
^EXIT=0$
7+
^SIGNAL=0$
8+
--
9+
--
10+
Note: disabled for paths-lifo mode, which never merges state guards
11+
This checks that an assume(false) prior to a do-while loop correctly results
12+
in symex stepping into a single iteration of that loop, and then out,
13+
resulting in the state guard returning to true.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
int main(int argc, char **argv)
2+
{
3+
if(argc == 1)
4+
{
5+
__CPROVER_assume(0);
6+
do
7+
{
8+
} while(1);
9+
}
10+
11+
assert(0);
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CORE
2+
test.c
3+
4+
^VERIFICATION FAILED$
5+
^EXIT=10$
6+
^SIGNAL=0$
7+
--
8+
--
9+
This tests that with a __CPROVER_assume(0) before an infinite loop,
10+
symex terminates without needing a `--unwind` setting.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CORE paths-lifo-expected-failure
2+
test.c
3+
--show-vcc
4+
^Generated 1 VCC\(s\), 1 remaining after simplification$
5+
^\{1\} false$
6+
^EXIT=0$
7+
^SIGNAL=0$
8+
--
9+
--
10+
Note: disabled for paths-lifo mode, which never merges state guards
11+
This checks that an assume(false) within a branch correctly results
12+
in symex returning the state guard to true when control flow converges.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
int main(int argc, char **argv)
2+
{
3+
if(argc == 1)
4+
{
5+
__CPROVER_assume(0);
6+
}
7+
else
8+
{
9+
}
10+
11+
assert(0);
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CORE
2+
test.c
3+
4+
^VERIFICATION FAILED$
5+
^EXIT=10$
6+
^SIGNAL=0$
7+
--
8+
--
9+
This tests that with a __CPROVER_assume(0) within conditional control flow,
10+
symex still executes the assertion as expected.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CORE paths-lifo-expected-failure
2+
test.c
3+
--show-vcc
4+
^Generated 1 VCC\(s\), 1 remaining after simplification$
5+
^\{1\} false$
6+
^EXIT=0$
7+
^SIGNAL=0$
8+
--
9+
--
10+
Note: disabled for paths-lifo mode, which never merges state guards
11+
This checks that an assume(false) prior to a branch correctly results
12+
in symex picking some arbitrary path through the unreachable CFG, and so
13+
it returns the state guard to true when control flow converges.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
int main(int argc, char **argv)
2+
{
3+
if(argc > 1)
4+
{
5+
__CPROVER_assume(0);
6+
7+
if(argc == 2)
8+
{
9+
++argc;
10+
}
11+
else
12+
{
13+
--argc;
14+
}
15+
}
16+
17+
assert(0);
18+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CORE
2+
test.c
3+
4+
^VERIFICATION FAILED$
5+
^EXIT=10$
6+
^SIGNAL=0$
7+
--
8+
--
9+
This tests that with a __CPROVER_assume(0) prior to conditional control flow,
10+
symex executes the final assertion as expected.

src/goto-symex/goto_state.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ class goto_statet
4949
// of the condition of the if).
5050
guardt guard;
5151

52+
/// Is this code reachable? If not we can take shortcuts such as not entering
53+
/// function calls, but we still conduct guard arithmetic as usual.
54+
bool reachable;
55+
5256
// Map L1 names to (L2) constants. Values will be evicted from this map
5357
// when they become non-constant. This is used to propagate values that have
5458
// been worked out to only have one possible value.
@@ -73,7 +77,7 @@ class goto_statet
7377
explicit goto_statet(const class goto_symex_statet &s);
7478

7579
explicit goto_statet(guard_managert &guard_manager)
76-
: guard(true_exprt(), guard_manager)
80+
: guard(true_exprt(), guard_manager), reachable(true)
7781
{
7882
}
7983

src/goto-symex/goto_symex.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,9 @@ class goto_symext
332332
/// Symbolically execute a GOTO instruction
333333
/// \param state: Symbolic execution state for current instruction
334334
virtual void symex_goto(statet &state);
335+
/// Symbolically execute a GOTO instruction in the context of unreachable code
336+
/// \param state: Symbolic execution state for current instruction
337+
void symex_unreachable_goto(statet &state);
335338
/// Symbolically execute a START_THREAD instruction
336339
/// \param state: Symbolic execution state for current instruction
337340
virtual void symex_start_thread(statet &state);

src/goto-symex/goto_symex_state.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ inline goto_statet::goto_statet(const class goto_symex_statet &s)
277277
level2(s.level2),
278278
value_set(s.value_set),
279279
guard(s.guard),
280+
reachable(s.reachable),
280281
propagation(s.propagation),
281282
atomic_section_id(s.atomic_section_id)
282283
{

src/goto-symex/symex_atomic_section.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Author: Daniel Kroening, [email protected]
1515

1616
void goto_symext::symex_atomic_begin(statet &state)
1717
{
18-
if(state.guard.is_false())
18+
if(!state.reachable)
1919
return;
2020

2121
if(state.atomic_section_id != 0)
@@ -35,7 +35,7 @@ void goto_symext::symex_atomic_begin(statet &state)
3535

3636
void goto_symext::symex_atomic_end(statet &state)
3737
{
38-
if(state.guard.is_false())
38+
if(!state.reachable)
3939
return;
4040

4141
if(state.atomic_section_id == 0)

src/goto-symex/symex_function_call.cpp

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,6 @@ void goto_symext::symex_function_call_code(
260260

261261
// Rule out this path:
262262
symex_assume_l2(state, false_exprt());
263-
// Disable processing instructions until we next encounter one reachable
264-
// without passing this instruction:
265-
state.guard.add(false_exprt());
266263
}
267264

268265
symex_transition(state);
@@ -356,18 +353,15 @@ static void pop_frame(
356353
// accumulate assumptions (in symex_assume_l2) and must be left alone.
357354
// If however it is single-threaded then we should restore the guard, as the
358355
// guard coming out of the function may be more complex (e.g. if the callee
359-
// was { if(x) __CPROVER_assume(false); } then the guard may still be `!x`),
356+
// was { if(x) while(true) { } } then the guard may still be `!x`),
360357
// but at this point all control-flow paths have either converged or been
361358
// proven unviable, so we can stop specifying the callee's constraints when
362359
// we generate an assumption or VCC.
363360

364-
// If the guard is false, *this* path is unviable and we shouldn't discard
365-
// that knowledge. If we're doing path exploration then we do
366-
// tail-duplication, and we actually *are* in a more-restricted context
367-
// than we were when the function began.
368-
if(
369-
state.threads.size() == 1 && !state.guard.is_false() &&
370-
!doing_path_exploration)
361+
// If we're doing path exploration then we do tail-duplication, and we
362+
// actually *are* in a more-restricted context than we were when the
363+
// function began.
364+
if(state.threads.size() == 1 && !doing_path_exploration)
371365
{
372366
state.guard = frame.guard_at_function_start;
373367
}

0 commit comments

Comments
 (0)