Deconstructing Dynamic Symbolic Execution
Deconstructing Dynamic Symbolic Execution
Execution
Thomas BALL a and Jakub DANIEL b
a
Microsoft Research
b
Charles University
1. Introduction
def max4(a,b,c,d):
return max2(max2(a,b),max2(c,d))
(that hopefully will cover a new path). This pseudo-code elides a number of details
that we will deal with later.
Consider the Python function max4 in Figure 2, which computes the max-
imum of four numbers via three calls to the function max2. Suppose we ex-
ecute max4 with values of zero for all four arguments. In this case, the ex-
ecution path p contains three comparisons (in the order (a < b), (c < d),
(a < c)), all of which evaluate false. Thus, the path-condition for path p is
(not(a<b) and not(c<d) and not(a < c)). Negating this condition yields
((a<b) or (c<d) or (a<c)). Taking the execution ordering of the three com-
parisons into account, we derive three expressions from the negated path-condition
to generate new inputs that will explore execution prefixes of path p of increasing
length:
length 0: (a<b)
length 1: not (a<b) and (c<d)
length 2: not (a<b) and not (c<d) and (a<c)
The purpose of taking execution order into account should be clear, as the com-
parison (a<c) only executes in the case where (not (a<b) and not (c<d))
holds.
Integer solutions to the above three systems of constraints are:
a == 0 and b == 2 and c == 0 and d == 0
a == 0 and b == 0 and c == 0 and d == 3
a == 0 and b == 0 and c == 2 and d == 0
In the three cases above, we sought solutions that kept as many of the variables
as possible equal to the original input (in which all variables are equal to 0). Exe-
cution the max4 function on the input corresponding to the first solution produces
the path-condition ((a<b) and not(c<d) and not(b < c)), from which we
can produce more inputs. For this (loop-free function), there are a finite number
of path-conditions. We leave it as an exercise to the reader to enumerate them
all.
We now consider several situations where we can make use of concrete values
in DSE. In the realm of (unbounded-precision) integer arithmetic (e.g., bignum
def fermat3(x,y,z):
if (x > 0 and y > 0 and z > 0):
if (x*x*x + y*y*y == z*z*z):
return "Fermat and Wiles were wrong!?!"
return 0
def dart(x,y):
if (unknown(x) == y):
return 1
return 0
1.3. Overview
This introduction elides many important issues that arise in implementing DSE
for a real language, which we will focus on in the remainder of the paper. These
include how to:
Identify the code under test P and the symbolic inputs to P ;
Trace the control flow path p taken by execution P (i);
Reinterpret program operations to compute symbolic expressions;
Generate a path-condition from p and the symbolic expressions;
Generate a new input i0 by negating (part of) the path-condition, translating
the path-condition to the input language of an ATP, invoking the ATP, and
lifting a satisfying model (if any) back up to the source level;
Guide the search to expose new paths.
The rest of this paper is organized as follows. Section 2 describes an instrumented
typing discipline where we lift each type (representing a set of concrete values)
to a symbolic type (representing a set of pairs of concrete and symbolic values).
Section 3 shows how strongest postconditions defines a symbolic semantics for
a small programming language and how strongest postconditions can be refined
to model DSE. Section 4 describes an implementation of DSE for the Python
language in the Python language that follows the instrumented semantics pat-
tern closely (full implementation and tests available at PyExZ3, tagged “v1.0”).
Section 5 describes the symbolic encoding of Python integer operations using two
decision procedures of Z3: linear arithmetic with uninterpreted functions in place
of non-linear operations; fixed-width bitvectors with precise encodings of most op-
erations. Section 6 offers a number of ideas for projects to extend the capabilities
of PyExZ3.
2. Instrumented Types
S → v := E
| skip
| S1 ; S2
| if E then S1 else S2 end
| while E do S end
The language of expressions (E) is defined by the application of operations to val-
ues, where constants (nullary operations) and program variables form the leaves
of the expression tree and non-nullary operators form the interior nodes of the
tree. For now, we will consider all values to be immutable. That is, the only source
of mutation in the language is the assignment statement.
To introduce symbolic execution into the picture, we can imagine that a type
T ∈ U has (one or more) counterparts in a symbolic universe U 0 . A type T 0 ∈ U 0
is a subtype of T ∈ U with two purposes:
First, a value of type T 0 represents a pair of values: a concrete value c of
(super)type T and a symbolic expression e. A symbolic expression is a tree
whose leaves are either nullary operators (i.e., constants) of a type in U or
are Skolem constants representing the (symbolic) inputs (v1 . . . vk ) to the
program P , and whose interior nodes represent operations from types in
U . We refer to Skolem constants as “symbolic constants” from this point
on. Note that symbolic expressions do not contain references to program
variables.
Second, the type T 0 redefines some of the operations o ∈ T , namely those
for which we wish to compute symbolic expressions. An operation o ∈ T 0
has the same parameter list as o ∈ T , allowing it to take inputs with types
from both U and U 0 . The return type of o ∈ T 0 generally is from U 0 (though
it can be from U ). Thus, o ∈ T 0 is a proper function subtype of o ∈ T . The
purpose of o ∈ T 0 is to: (1) perform operation o ∈ T on the concrete values
associated with its inputs; (2) build a symbolic expression tree rooted at
operation o whose children are the trees associated with the inputs to o.
Figure 5 presents pseudo code for the instrumentation of a type T via a type T 0 .
The class Symbolic is used to hold an expression tree (Expr). Given a class T ∈ U ,
a symbolic type T 0 ∈ U 0 is defined by inheriting from both T and Symbolic. This
ensures that a T 0 can be used wherever a T is expected.
A type such as T 0 only can be constructed by providing a concrete value c of
type T and a symbolic expression e to the constructor for T 0 . This will be done
in exactly two places:
by the creation of symbolic constants associated with the primary inputs
(v1 . . . vk ) to the program;
by the instrumented operations as shown in Figure 5.
Figure 5. Type instrumentation to carry both concrete values and symbolic ex-
pressions.
The previous section showed how symbolic expressions can be computed via a
set of instrumented types, where the expressions are computed as a side-effect
of the execution of program operations. This section shows how these symbolic
expressions can be used to form a path-condition (which then can be compiled into
a logic formula and passed to an automated theorem prover to find new inputs
to drive a program’s execution along new paths). We derive a path-condition
directly from the strongest postcondition (symbolic) semantics of our programming
language, refining it to model the basic operations of an interpreter.
4
1. SP (P , x := E ) = ∃y . (x = E [ x → y ]) ∧ P [ x → y ]
4
2. SP (P , skip) = P
4
3. SP (P , S1 ; S2 ) = SP (SP (P , S1 ), S2 )
4
4. SP (P , if E then S1 else S2 end) =
SP (P ∧ E , S1 ) ∨ SP (P ∧ ¬E , S2 )
4
5. SP (P , while E do S end) =
SP (P , if E then S ; while E do S end else skip end)
Rule (1) defines the strongest postcondition for the assignment statement. The
assignment is modelled logically by the equality x = E where any free occurrence
of x in E is replaced by the existentially quantified variable y, which represents
the value of x in the pre-state. The same substitution ([x → y]) is applied to the
pre-state predicate P .
Rules (2)-(5) define the strongest postcondition for the four control-flow state-
ments. The rules for the skip statement and sequencing (;) are straightforward.
Of particular interest, note that the rule for the if-then-else statement splits cases
on the expression E. It is here that DSE will choose one of the cases for us, as
the concrete execution will evaluate E either to be true or false. This gives rise
to the path-condition (either P ∧ E or P ∧ ¬E). The recursive rule for the while
loop unfolds as many times as the expression E evaluates true, adding to the
path-condition.
Thus, we see that the initial value of every input variable is characterized by a
symbolic constant sci or constant ci . We assume that every non-input variable in
the program is initialized before being used.
The strongest postcondition is formulated to deal with open programs, pro-
grams in which some variables are used before being assigned to. This surfaces in
Rule (1) for assignment, which uses existential quantification to refer to the value
of variable x in the pre-state.
By construction, we have that every variable is defined before being used.
This means that the precondition P can be reformulated as a pair < σ, Pc >,
where σ is a store mapping variables to values and Pc is the path-condition, a list
of symbolic expressions (predicates) corresponding to the expressions E evaluated
in the context of an if-then-else statement. Initially, we have that :
representing the initial condition Init, and Pc = [], the empty list. We use σ 0 to
refer to the formula that the store σ induces:
^
σ0 = (v = V ) (3)
(v,V )∈σ
Thus, the pair < σ, Pc > represents the predicate P = σ 0 ∧ ( c∈Pc c). A store
V
σ supports two operations: σ[x] which denotes the value that x maps to under
σ; σ[x 7→ V ], which produces a new store in which x maps to value V and is
everywhere else the same as σ.
Now, we can redefine strongest postcondition for assignment to eliminate the
use of existential quantification and model the operation of an interpreter, by
separating out the notion of the store:
4
1. SP (< σ, Pc >, x := E ) = < σ[x 7→ eval (σ, E )], Pc >
where eval(σ, E) evaluates expression E under the store σ (where every occurrence
of a free variable v in E is replaced by the value σ[v]). This is the standard
substitution rule of a standard operational semantics.
We also redefine the rule for the if-then-else statement so that it chooses which
branch to take and appends the appropriate symbolic expression (predicate) to
the path-condition Pc :
4
4. SP (< σ, Pc >, if E then S1 else S2 end) =
let choice = eval (σ, E ) in
if choice then SP (< σ, Pc :: expr (choice) >, S1 )
else SP (< σ, Pc :: ¬expr (choice) >, S2 )
3.3. Summing it up
We have shown how the symbolic predicate transformer SP can be refined into
a symbolic interpreter operating over the symbolic types defined in the previous
section. In the case when every input variable is symbolic and every operator is
redefined, the path-condition is equivalent to the strongest postcondition of the
execution path p. This guarantees that the path-condition for p is sound. In the
case where a subset of the input variables are symbolic and/or not all operators
are redefined, the path-condition of p is not guaranteed to be sound. We leave
it as an exercise to the reader to establish sufficient conditions under which the
use of concrete values in place of symbolic expressions is guaranteed to result in
sound path-conditions.
This section does not address the compilation of a symbolic expression to the
(logic) language of an underlying ATP, nor the lifting of a satisfying assignment
to a formula back to the level of the source language. This is best done for a
particular source language and ATP, as detailed in the next section.
4. Architecture of PyExZ3
In this section we present the high-level architecture of a simple DSE tool for the
Python language, written in Python, called PyExZ3. Figure 6 shows the class dia-
gram (dashed edges are “has-a” relationships; solid edges are “is-a” relationships)
of the tool.
The Loader class takes as input the name of a Python file (e.g., foo.py)
to import. The loader expects to find a function named foo inside the file
foo.py, which will serve as the starting point for symbolic execution. The
FunctionInvocation class wraps this starting point. By default, each parameter
to foo is a SymbolicInteger unless there is decorator @symbolic specifying the
type to use for a particular argument.
The loader provides the capability to reload the module foo.py so that
the function foo can be reexecuted within the same process from the same
initial state with different inputs (see the class ExplorationEngine) via the
FunctionInvocation class.
Finally, the loader looks for specially named functions expected result
(expected result set) in file foo.py to use as a test oracle after the path explo-
ration (by ExplorationEngine) has completed. These functions are expected to
return a list of values to check against the list of return values collected from the
executions of the foo function. The presence of the function expected result
(expected result set) yields a comparison of the two lists as bags (sets). We use
such weaker tests, rather than list equality, because the order in which paths are
explored by the ExplorationEngine can easily change due to small differences
in the input programs.
As we have explained DSE, the symbolic expressions are represented at the level
of the source language. As detailed later in Section 5, we must translate from the
source language to the input language of an automated theorem prover (ATP),
in this case Z3. This separation of languages is quite useful, as we may have the
need to translate a given symbolic expression to the ATP’s language multiple
times, to make use of different features of the underlying ATP. Furthermore, this
separation of concerns allows us to easily retarget the DSE tool to a different
ATP.
The base class Z3Expression represents a Z3 formula. The two subclasses
Z3Integer and Z3BitVector represent different ways to model arithmetic rea-
soning about integers in Z3. We will describe the details of these encodings in
Section 5.
The class Z3Wrapper is responsible for performing the translation from the
source language (Python) to Z3’s input language, invoking Z3, and lifting a Z3
answer back to the level of Python. The findCounterexample method does all
the work, taking as input a list of Predicates (called assertions) as well as a
single Predicate (called the query). The assertions represent a path-condition
prefix derived from the Constraint tree that we wish the next execution to follow,
while query represents the predicate following the prefix in the tree that we will
negate.
The method constructs the formula
^
( a) ∧ ¬query (4)
a∈asserts
We have presented the basics of dynamic symbolic execution (for Python). A more
thorough treatment would deal with other data types besides integers, such as
Python dictionaries, strings and lists, each of which presents their own challenges
for symbolic reasoning. There are many other interesting challenges in DSE, such
as dealing with user-defined classes (rather than built-in types as done here) and
multi-threaded execution.
7. Acknowledgements
Many thanks to the students of the 2014 Marktoberdorf Summer School on De-
pendable Software Systems Engineering for their questions and feedback about
the first author’s lectures on dynamic symbolic execution. The following students
of the summer school helpfully provided tests for the PyExZ3 tool: Daniel Dar-
vas, Damien Rusinek, Christian Dehnert and Thomas Pani. Thanks also to Peter
Chapman for his contributions.
References
[1] Cristian Cadar and Dawson R. Engler. Execution generated test cases: How to make
systems code crash itself. In Proceedings of 12th International SPIN Workshop, pages
2–23, 2005.
[2] Cristian Cadar and Koushik Sen. Symbolic execution for software testing: three decades
later. Communications of the ACM, 56 (2): 82–90, 2013.
[3] Cristian Cadar, Vijay Ganesh, Peter M. Pawlowski, David L. Dill, and Dawson R. En-
gler. EXE: automatically generating inputs of death. In Proceedings of the 13th ACM
Conference on Computer and Communications Security, pages 322–335, 2006.
[4] Lori A. Clarke. A system to generate test data and symbolically execute programs. IEEE
Transactions on Software Engineering, 2 (3): 215–222, 1976.
[5] Leonardo Mendonça de Moura and Nikolaj Bjørner. Z3: an efficient SMT solver. In
Proceedings of the 14th International Conference of Tools and Algorithms for the Con-
struction and Analysis of Systems, pages 337–340, 2008.
[6] Edsger W. Dijkstra. A Discipline of Programming. Prentice-Hall, 1976.
[7] Patrice Godefroid. Higher-order test generation. In Proceedings of the ACM SIGPLAN
Conference on Programming Language Design and Implementation, pages 258–269, 2011.
[8] Patrice Godefroid, Nils Klarlund, and Koushik Sen. DART: directed automated random
testing. In Proceedings of the ACM SIGPLAN Conference on Programming Language
Design and Implementation, pages 213–223, 2005.
[9] Patrice Godefroid, Michael Y. Levin, and David A. Molnar. SAGE: whitebox fuzzing for
security testing. Communications of the ACM, 55 (3): 40–44, 2012.
[10] Neelam Gupta, Aditya P. Mathur, and Mary Lou Soffa. Generating test data for
branch coverage. In Proceedings of the Automate Software Engineering Conference, pages
219–228, 2000.
[11] James C. King. Symbolic execution and program testing. Communications of the ACM,
19 (7): 385394, 1976.
[12] Bogdan Korel. Automated software test data generation. IEEE Transactions on Software
Engineering, 16 (8): 870–879, 1990.
[13] Bogdan Korel. Dynamic method of software test data generation. Journal of Software
Testing, Verification and Reliability, 2 (4): 203–213, 1992.
[14] Koushik Sen and Gul Agha. CUTE and jcute: Concolic unit testing and explicit path
model-checking tools. In Proceedings of 18th Computer Aided Verification Conference,
pages 419–423, 2006.