{'fraction_non_alphanumeric': 0.028404041733864332, 'fraction_numerical': 0.009084027252081756, 'mean_word_length': 4.871304347826087, 'pattern_counts': {'":': 0, '<': 16, '<?xml version=': 0, '>': 1, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
Distributed Symbolic Execution using Test-Depth Partitioning

Symbolic execution is a classic technique for systematic bug finding, which has seen many applications in recent years but remains hard to scale. Recent work introduced ranged symbolic execution to distribute the symbolic execution task among different workers with minimal communication overhead using test inputs. However, each worker was restricted to perform only a depth-first search. This paper introduces a new approach to ranging, called test-depth partitioning, that allows the workers to employ different search strategies without compromising the completeness of the overall search. Experimental results show that the proposed approach provides a more flexible ranging solution for distributed symbolic execution. The core idea behind test-depth partitioning is to use a test-depth pair to define a region in the execution space. Such a pair represents a partial path or a prefix, and it obviates the need for complete tests to determine boundaries as was the case in the previous ranging scheme. Moreover, different workers have the freedom to select the search strategy of choice without affecting the functioning of the overall system. Test-depth partitioning is implemented using KLEE, a well-known symbolic execution tool. The preliminary results show that the proposed scheme can prove to be an efficient tool to speed up symbolic execution.


Introduction
Symbolic execution, conceptualized and demonstrated almost 40 years ago [20,8], is one of the most versatile and influential methodologies for analyzing software. The core of symbolic execution is a technique that undertakes a structured exploration of the execution paths which exist in a given program. A standard symbolic execution tool comprises of two main components. The first component constructs the path conditions, which are constraints on program inputs that cause the execution of a particular path. The second component is a mechanism to solve the path conditions and provide concrete values to the program inputs. Symbolic execution has found widespread application in test input generation. Solving path conditions for execution paths in a program yields a capable test suite which provides better code coverage. Advancements in SAT and SMT solving technology coupled with the rapid rise of computing power has paved the way for using symbolic execution in a diverse range of real-world scenarios.
While symbolic execution presents itself as an attractive tool to test various aspects of software design, it suffers from scalability issues. The main reasons for this drawback are the complexity of path conditions and the state space explosion as software becomes more extensive and more expressive. These factors lead to prohibitively high exploration times, which hinders the adoption of this technology. There have been several research endeavors to address these bottlenecks. Concolic execution, introduced in DART [19], combines concrete and symbolic execution to limit the path explosion problem. In this technique, the program runs with program inputs having concrete values. The symbolic path conditions for the execution path are recorded and the last branch in the path condition is negated to execute a new path. Several tools extend concolic execution to target different programming environments and provide additional features [26,27,10]. DiSE [22], using an incremental approach, symbolically executes only the modified parts of the code. The compositional approach used in SMART [18] uses concolic execution for functions in isolation to reduce the number of paths. The distributed approach, our focus in this work, involves breaking down a large symbolic execution problem into smaller ones and solving them in a parallel/distributed fashion. Cloud9[9,15] is a popular tool which carries out distributed symbolic execution and has been applied to test real-world systems. Distributed approach has also been used on Java bytecode [31,24].
Several modern symbolic solvers incorporate the Execution Generated Test [12] approach where both concrete and symbolic states of a program are maintained. If an operation involves all concrete values, then it is executed normally, but in the presence of one or more symbolic variables, symbolic execution takes place. EXE [13] and KLEE [11] are two well-known tools in this category. KLEE is an open source symbolic execution tool that works on LLVM [5] generated bytecode and integrates with the LLVM architecture. KLEE, and tools built on top of KLEE have been used to test a wide range of software applications like computer vision [16], sensor networks [25], GPUs [21], device drivers [14], and online gaming [7]. KLEE is also used for automated debugging [33], thread scheduling [17], and exploit generation [6]. We use KLEE to implement the framework proposed in this paper.
Siddiqui and Khurshid [29,28] introduced a novel approach to distributed symbolic execution called ranged analysis. Their approach involved using a test case to represent a state of the execution process. Ranging using tests, referred to as test ranging, presents an innovative technique to parallelize symbolic execution. This paper builds upon the idea of using test inputs to define distinct regions in the program space. Test ranging allows for using only one type of search strategy -depth-first search(DFS) which can be restrictive, especially when trying to maximize coverage in a limited amount of time. Having different search strategies also help develop better testing policies. This type of ranging also requires full test paths to define boundaries. To address these limitations, we introduce an alternative methodology to accomplish ranged analysis called test-depth partitioning. Our key observation is that the partial paths, also called prefixes, can be used to define non-overlapping and exhaustive ranges in the program space and a prefix can be compactly represented as a test-depth pair. A single test-depth pair represents a pool of execution paths, generated by extending the corresponding prefix. These paths are distinct from execution paths made by any other prefix at the same depth. This inherent partitioning can be exploited to define ranges based on these test-depth pairs which can be explored independently and using the search technique of choice. A worker executing a region defined by a pair, at any stage, has a set of candidate paths that need to be explored. The candidate selection criterion depends on the search strategy. When asked to offload work for load balancing, one of the candidate paths is converted to a test and is paired with the current depth of the path to make a test-depth couple. This pair represents a prefix which defines a region that can be explored by another worker. This paper makes the following contributions.
-Test-depth partitioning. We use test-depth pairs to represent well-defined regions in program space and develop a method to restrict symbolic execution to this region.
-Parallel symbolic execution with different search strategies. Using test-depth partitioning, we devise a distributed symbolic execution scheme which enables several workers to symbolically explore distinct regions of the program space in parallel, employing the search strategy of choice.
-Implementation. We construct a working prototype of our proposed framework using KLEE as the symbolic execution platform. Message passing interface (MPI) [30] is used to implement distributed execution and load balancing. Out tool is publicly available at: https://github.com/Shikhar8990/test-depth-partitioning -Evaluation. To assess the performance of the proposed framework, we use two sets of applications as our test subjects. The first set comprises twenty-five programs from the GNU Core utilities[2]. The second set consists of three software libraries that perform operations such as processing YAML data and implementing an arbitrary precision calculator. An evaluation was carried out to determine the reduction in symbolic execution times of the subjects and analyzing the impact of the factors such as the number of participating workers and the search strategies used.

Conceptual Overview
This section describes the basic symbolic execution process and illustrates the core ideas of our framework. The primary objective of symbolic execution is to explore all possible execution paths in a given program. In contrast to concrete execution where variables are assigned explicit values, symbolic execution makes one or more input variables symbolic and reasons on the values of these variables for different execution paths. To illustrate the symbolic exploration procedure and demonstrate the functioning of the proposed system, we use a simple C function that returns the middle integer out of three given integers. It contains six possible execution paths. Each path is taken under a specific value or range of values of the input integers, and these restrictions on the inputs generate the path conditions. These path conditions can be provided to a constraint solver to determine actual values of symbolic variables to execute a particular path. A bitvector, where a 0 stands for a false branch while 1 denotes a true branch, can represent every path in the execution tree. The location of a bit in the bit-vector represents the depth of the branch. We refer to such vectors as test paths. Table 1 list all test paths in the program and their associated constraints.

Test Path
Constraints Output T1 01 (!(x<y) and (x<z)) x T2 11 ((x<y) and (y<z)) y T3 000 (!(x<y) and !(x<z) and !(y<z)) y T4 001 (!(x<y) and !(x<z) and (y<z)) z T5 100 ((x<y) and !(y<z) and !(x<z)) x T6 101 ((x<y) and !(y<z) and (x<z)) z Tests represent full paths as they trace the execution from beginning to end. A prefix is a partial path that starts at the program entry block but does not go all the way to a termination/exit block. The given program has two feasible prefixes at an execution depth of two (00, 10). A test-depth pair can be used to represent a prefix; (T3,2) and (T4,2) represent prefix 00 while (T5,2) and (T6,2) represent prefix 10. Prefixes can become very long for extensive programs and test-depth pairs can provide a compact representation for partial program paths. The technique presented in this paper uses these test-depth pairs to define non-overlapping and exhaustive regions, which can be processed individually as smaller sub-problems. Symbolic analysis, using a test-depth pair, happens in two phases. First, the provided test determines the input value(s) to the program, that are used to trace the path taken by the test up to the given depth (provided in the test-depth pair). This phase does not require any constraint solving as test inputs decide the branching decisions. Once execu-tion reaches the assigned depth; the second phase begins with a symbolic exploration of all possible program paths from that depth level onwards. In this example, a worker, assigned the pair (T3/T4,2), will end up finding paths 000 and 001 while another worker, with (T5/T6,2), will find paths 100 and 101. Pairs (T1,2) and (T2,2) represent test paths and can not be extended further.  Figure 1a depicts the execution tree of the find_middle subroutine. Red and black arrows indicate false and true branches respectively. As has already been discussed, the code contains six full paths which produces six tests. The symbolic execution process starts from the first line of the program and generates an initial state a. When execution hits the first branch, two statesb and c, representing the if and the fall-through branches are generated. Subsequent states labeled d to k are spawned through the course of execution. Each state keeps a track of the path constraints required to trace the path it represents. Let us assume a scenario where a worker starts execution with a test-depth pair of {{x=1,y=0,z=0},2}. Figure 1b shows the execution path of this worker. It relies on the test inputs to guide its execution for the first two branches as the given depth in the pair is two. On the first branch, it will only explore the false branch and suspend state c which is not satisfied by the test. Similarly, the second branch would only add state d while suspending e. The execution sub-trees beginning at the suspended states are not explored. When this worker receives another test-depth pair -{{x=0,y=1,z=2},2}, the suspended states are searched to find a state whose constraints are satisfied by the new test. In this case, the test inputs satisfy state c, and it gets activated for further exploration. Since the provided depth is two, the test would have guided the next branch as well, adding state g and suspending f.

Bounding program space using test-depth pairs
To accomplish test-depth partitioning, we modify the standard symbolic execution algorithm to allow for a test to guide the execution up to a certain depth and then explore all possible paths below that depth. Algorithm 1 describes this augmentation. The exploration process represents the program paths as execution states. The execution on reaching a branch generates states to capture both the true and false branches (B B _t r ue and B B _ f al se)(lines 3-4). When restricting the program space using a test-depth pair (τ, τ_d ept h), on hitting a branch, one of two things can happen. If the execution depth is less than test depth (τ_d ept h), we let the test inputs (τ) decide the branch and suspend the state corresponding to the not-taken branch (lines 6-12). The function SOLV E P AT H assigns the test inputs to the branch condition and determines the branching decision. Since a concrete test will always take only one branch, this ensures only one specific path gets replayed till τ_d ept h. Standard symbolic execution resumes once execution reaches the assigned region (deeper than τ_d ept h) and satisfiability of branch conditions (cond ) determines path exploration (lines [14][15][16][17][18][19].

Distributed symbolic execution
This section discusses the distributed symbolic execution framework to enable different workers to explore non-overlapping parts of the program space. The setup comprises a coordinator node and several worker nodes. The coordinator is responsible for generating a pool of test-depth pairs and assigning them to the worker nodes. Each worker symbolically executes the paths in region of the assigned pair. The workers have the freedom to decide the search strategy. This method also overcomes a major limitation of previous work on ranging, which could only use depth-first search. Load balancing happens through work stealing. Workers can quickly convert an unexplored path into a test-depth pair and send it to the coordinator which sends it to the idle worker.
Coordinator operation The coordinator node is responsible for assigning and managing work amongst worker nodes. Algorithm 2 shows a simplified version of the tasks performed by the coordinator. Initially, it undertakes a shallow symbolic execution to generate an initial pool of test-depth pairs (line 6) to distribute. The current configuration assigns one pair to each worker (lines 7-11). The coordinator also keeps track of busy and idles workers ( f r ee_Li st , bus y_Li st ). After the initial distribution of work, it monitors messages from the worker nodes (line 13). If a worker finishes exploring its assigned region (line 14), the busy and idle lists get updated (lines [15][16]. The search terminates when all workers become idle (line 18). In case a worker becomes available, the coordinator asks one of the busy workers to provide work to the idle worker in the form of another test-depth pair (lines 21-24). The configuration shown explores up to a finite depth ( f i nal _d ept h). However, the framework also supports time bounded exploration. The search policy determines the searchType.
Worker operation Algorithm 3 describes the various operations carried out by the workers. These nodes, responsible for carrying out symbolic execution in their assigned regions, maintain a list of states that are active and can be explored further and states that are suspended. When a worker receives a new task from the coordinator (line 5), it can start from the program entry point if this is the first test-depth pair (lines 7-9), or it can resume one of the suspended states, whose constraints are satisfied by τ (lines [13][14], and explore it further (lines [16][17][18]. The worker node notifies the coordinator after completion of the assigned task (lines 10,19

Benchmarks
The proposed framework is evaluated using GNU Coreutils (version 6.11)[2] and three libraries. Coreutils are one of the most heavily used tools in Unix-like systems and deal with shell, text, and file system operations. We use twenty-five programs with the largest LLVM bit-code files from this suite. The second set of subjects comprise the GNU oSIP 4.0.0[3], GNU BC 2. 27[1], and LibYAML 0.1.5 [4] libraries. KLEE has to process multiple source files to analyze these applications. oSIP implements the Session Initiation Protocol (SIP) and provides an API for applications to incorporate the protocol. BC is used to perform arbitrary precision numeric processing. Its syntax is similar to C, and it can be used either as an interactive calculator or as a scripting language. LibYAML provides the functionality to parse and process data in YAML format, which is a serialization format designed to be human-readable. The execution drivers for these libraries are adapted from [32].

Methodology
To demonstrate the efficacy of our scheme, we observe the speed-ups achieved when using multiple workers. For a defined program space, we compare the time taken to complete the symbolic execution when using one worker with the execution times when using more than one worker. We evaluate two distributed configurations -one comprising two worker nodes and the other comprising four workers. For a given test application, the exploration space has to be consistent across configurations. To accomplish this, we run a breadth-first search(BFS) for all the programs with a timeout of six-hundred seconds using one worker. The depth to which symbolic execution reaches before timeout provides the exploration upper-bound. For subsequent experiments analyzing the run-times, execution terminates when all states across all workers reach this depth which acts as a termination criterion. This results in a well-defined bounded exploration space. To make the evaluation comprehensive, we use three exploration strategies -depth-first, breadth-first, and random-state search. The search technique determines how states are explored. Depth-first strategy, upon reaching a branching point, forks execution and follows one branch till a termination point and then backtracks, generating deeper states in the process. Breadth-first search processes all states at a particular depth before proceeding to the next depth. Random-state search, as the name suggests, randomly picks a state to execute from the pool of active states. KLEE provides DFS and random-state search capabilities but lacks support for BFS. We developed a custom breadth-first search option to enable experimentation. Table 2 lists details of the hardware platform and software versions used for carrying out the experiments.

Results
This section discusses the results of experiments aimed to determine the speed-ups achieved when exploring a defined region of program space using test-depth partitioning. As already mentioned, we use twenty-five programs from GNU Coreutils and   Table 3 list these applications, the depth of exploration, and the total paths explored. We also list the number of test transfers that take place due to load balancing, when using a four-worker configuration. The resulting speed-ups discussed in the following sections are a result of the parallelism gained by a simultaneous exploration of different parts of the execution tree. The primary overhead of a distributed scheme like ours is that of test-depth pair transfers between workers during load balancing. The benefit of such a transfer depends on the size of the execution tree it represents. A test-depth transfer is beneficial if the communication and processing overhead is more than compensated by the parallelism gained. Transferring a test-depth pair, which represents an ample execution space to explore is more advantageous than a pair which terminates at a shallow depth. A significant challenge is that it is tough to make an apriori assessment of the size of the execution space represented by a test-depth pair. To alleviate this issue, in our scheme, when a worker is asked to offload one unexplored path to another worker, it gives preference to active states at shallow depths. A state near to the root of the tree is likely to represent a larger space than one farther away from it. Another factor impacting the observed speed-ups is the constraint-caching mechanism that KLEE uses where queries sent to the SMT solver are cached and reused for other paths. Calls to the solver are primary contributors to the execution times, and caching eliminates redundant SMT solving. Partitioning scheme like ours can also impact cache hit rates. Caching might not be very useful for a worker executing a series of test-depth pairs that represent paths lying in very different regions of the program space while it will benefit a worker executing on regions in close proximity. Caching can also be impacted by the search technique used. The overall performance of test-depth partitioning depends on the interplay between these factors which vary from application to application.
Coreutils Figure 2 shows the speed-ups delivered across two different configurations comprising two and four workers and three search techniques. Depth-first and random-state searches achieve median linear speed-ups when using two workers and an almost linear speed-up of 3.7x and 3.6x respectively when using four workers. These results indicate narrow execution trees for these programs. Using four workers and DFS for program cp results in a fairly balanced partitioning with only a few testdepth transfers (16) due to load balancing, which results in a speed-up of almost 6x. This application also witnesses a linear speed-up with random-state search. However, using BFS decreases the speed-up to 3.4x. The program ptx achieves a speedup of more than 6x with four workers and a random-state search. Eleven programs with DFS and seven programs with a random-state search see more than 4x speedup when executed with four workers. With two workers, twelve and fourteen programs see super-linear improvements with DFS and random search respectively. A few applications like ls, sum and split do not benefit a lot from parallel execution. In these cases, the benefits of distributing work are overshadowed by overheads associated with inter-worker communication and a possible reduction in constraint caching performance. In fact, for dir and ls, the execution times increase when going from two to four workers due to these overheads. Coreutils programs also display a marked diversity in behavior with speed-ups ranging from 1x to more than 6x across different configurations. Libraries The three application libraries are analyzed using a methodology similar to the one used for Coreutils programs. Figure 3 depicts the speed-ups achieved across three search techniques and two configurations. Depth-first search provides a median speed-up of 1.7x and 3.2x for two and four worker configurations, respectively. Random-state performs similar to DFS for the two core configurations while improving the four worker median speed-up to 3.85x for the three subjects. Breadthfirst search exhibits median speed-ups of 1.33x with two workers and 3.2x for four workers. A key observation is that the one search strategy might not always be the best across the board. For eg., libYAML, with four workers employing DFS, sees a 7x speed-up while bc, with the same configuration, sees only a 3.13x speed-up. However, if the search strategy changes to random-state, bc witnesses a higher speed-up (7x) than libYAML(3.85x). oSIP benefits the least from our partitioning and distributing scheme with a maximum speed-up of 3.3x with four workers using BFS.

Related Work
There have been several approaches to enable distributed symbolic execution. The current work builds upon the concept of ranged symbolic execution [29,28], which partitions the program space using tests. In the original work, a total ordering on tests is used to define ranges which start at a particular test τ1 and end at a test τ2 where τ2>τ1 according to the ordering. A range comprises paths that lie between the two bounding tests. This idea is extended to devise a distributed approach where different workers explore ranges defined by a pair of tests. Load balancing is accomplished using work-stealing. When a worker, performing execution on a given range, hits a branch, it explores the true branch puts the false one in a queue. Other workers can steal from this queue. If a node receives a request to offload work, one of the states in the queue is converted to a test case and passed to the coordinator, and its current range is modified to end at that test. For example, when a worker, executing a region between tests τ1 and τ2, receives a request to offload work. It uses one of its false branch states in the queue to a construct a test case τ3; it offloads the range between τ3 and τ2 while its range updates to one between τ1 and τ3. Nonoverlapping and exhaustive ranges are guaranteed as long as all the workers explore the true branch before the false branch, which enforces a depth-first strategy. Testdepth partitioning overcomes this limitation by incorporating the depth of a state to enable distribution of partial paths. Cloud9[9], a well known framework, defines region boundaries in terms of fence nodes and the pool of states to be explored comprise the candidate nodes. Participating workers get a part of the execution tree, and work transfers involve sending explicit paths, represented as bit-vectors, of candidate of nodes which get added to the sub-tree of the worker receiving these paths. Instead of representing paths as bit-vectors, our scheme represents them using testdepth pairs. Simple Static Partitioning(SSP) [31] applies parallel symbolic execution on Java bytecode. This technique builds on top of Symbolic PathFinder [23] as the symbolic execution framework. SSP performs a shallow symbolic search and collects path constraints for the explored paths. These constraints are then distributed among workers to act as pre-conditions for symbolic execution. Disjoint and complete constraints ensure that the partitioning is non-overlapping and every path is visited. SSP requires processing the recorded constraints to collect the frequency of each variable and identifying them as cheap or expensive. Preference is given to constraints that are cheaper to solve. As the name suggests, this scheme does not support load balancing.

Limitations and Future Work
A significant challenge of using a distributed scheme like ours is that the outcomes are dependent on program characteristics. As seen with the test subjects, most programs benefit from partitioning and distribution while a few of them do not see significant improvements in execution speeds. We try to address this issue by making the test suite as diverse as possible, and we plan to study and gain insights as to the program features that determine its symbolic execution performance in a distributing setting like ours. The current implementation of our scheme doesn't differentiate between execution paths when offloading work. As has been discussed in previous sections, the impact of distribution and load balancing depends on the size of the execution space and the nature of solver queries that lie in the region covered by a test-depth pair. To increase the efficacy of a scheme like test-depth partitioning, it is essential that only the paths, for which the communication overheads are more than compensated by added parallelism, be used for distribution. We attempt to alleviate this bottleneck by prioritizing shorter execution paths for transfers that happen due to load balancing. However, using program analysis techniques to devise intelligent partitioning methods is a research avenue we look forward to exploring. Many applications display different behaviour during different phases of execution. Several subject programs used for experimentation comprise an initial phase where the inputs are parsed and checked for validity and then processed in subsequent stages.
Another extension of the current work can be to identify phases of an application that benefit the most from parallelism and selectively distribute only those parts.

Conclusions
This paper presents a new approach to ranging based distributed symbolic execution called test-depth paritioning. We build this system using a symbolic execution tool called KLEE. The proposed scheme uses test-depth pairs, representing partial paths, to define non-overlapping regions in the execution tree of a program. These pairs are given to the workers to carry out symbolic execution in parallel. Our system allows for load balancing using work-stealing in which partially explored paths are converted to test-depth pairs that are transferred from a busy worker to an idle worker. Our scheme overcomes a major limitation of previous work on ranging by removing the restriction of using only depth-first search to carry out symbolic exploration. We evaluate our implementation using twenty-five programs from GNU Coreutils and three application libraries -bc, osip, and libYAML. Our findings demonstrate test-depth partitioning to be a promising distributed symbolic execution framework achieving median linear speed-ups when using depth-first and random-state searches with two workers and almost linear speed-ups of 3.7x with four workers for the Coreutils programs. The three libraries, using random-state search, witness median speedups of 1.73x and 3.85x with two and four workers respectively while a depth-first search provides median speed-ups of 1.7x and 3.14x for the two configurations respectively.