In this section, we first introduce the core components of ParaTaintGX, with a primary focus on its taint analysis architecture. We then illustrate how ParaTaintGX constructs call chains by treating the Function and Parameter Taint State as the fundamental analysis unit and leveraging a multi-node heuristic priority search algorithm. Finally, we present an efficient pruning strategy for call chain backtracking.
4.2. Call Chain Construction Based on Function and Parameter Taint State as the Fundamental Unit
In traditional static analysis, a call graph is modeled as a directed graph . The vertex set V contains nodes representing program functions, where each node corresponds to a function identifier .The edge set E consists of directed edges, and an edge = denotes that function invokes function . A call chain is defined as a path within the graph G, typically denoted as <, , …, >, meaning that function calls , calls , and this continues until reaching .
However, traditional static analysis methods alone cannot capture information about parameter taint states during call chain construction and backtracking. To address this limitation, we design a call chain node that uses the Function and Parameter Taint State as its basic unit. We define a more fine-grained call chain node in the call graph as , represented as a sextuple:
f: The function identifier, consistent with the meaning of a call chain node in traditional call graphs.
: A taint bit vector indicating the taint states of the n parameters of function f, where a value of 1 indicates a tainted parameter, while 0 indicates a clean parameter.
: The total number of tainted parameters in the current function.
: The length of the shortest path in the call chain from the taint source to function f.
: The number of direct successor nodes reachable from the current node.
denotes the propagation path from to , and : Indicates which function is called the current function and records the corresponding propagation path.
This sextuple design allows precise tracking of parameter taint states, improving the accuracy of call chain construction and backtracking in static analysis of SGX applications.
The structure of ParaTaintNode is illustrated in Listing 4. The fields taintsNum (number of tainted parameters), depth (shortest call depth from the taint source), and callNum (number of successor ParaTaintNodes) are primarily used in our multi-node heuristic priority search algorithm. The taintBitMap and address fields record the taint positions of the function, which are utilized to determine whether a ParaTaintNode has already been created during call chain construction for pruning purposes, and to assist the pruning process during call chain backtracking.The preParaTaintNodes field stores predecessor nodes to facilitate call chain backtracking and to maintain the corresponding taint propagation paths.
| Listing 4. ParaTaintNode Structure. |
![Mathematics 14 01007 i004 Mathematics 14 01007 i004]() |
4.2.1. ParaTaintNode for Mitigating Pruning False Negatives
During call chain construction, the complexity of function call relationships, including recursive and mutually recursive calls, often leads to loops and duplicate paths, which reduces the efficiency of call chain construction. To address this, the traditional call chain pruning strategy avoids revisiting nodes that have already been visited. Formally, given a call chain , if the next node to be added satisfies such that for some , then is pruned.
However, if pruning relies solely on functions as the basic unit, it may reduce duplicate paths and cycles, but it treats a function as a unique identifier and ignores that the same function may exhibit different behaviors under different parameter taint conditions. As a result, this strategy can lead to false negatives in the analysis.
As illustrated in
Figure 5 and Listing 5, after the first invocation of e_get_result, subsequent functions such as e_mpz_add are executed, and result is updated to point to a valid internal enclave buffer with an actual length of len_result. When e_get_result is invoked again, it copies the contents of this enclave buffer to external memory. If an attacker can influence the parameter len, i.e., if len becomes tainted, this second invocation may result in an out-of-bounds read from the enclave buffer. However, under a function-level call chain construction strategy, this subsequent invocation of e_get_result is pruned for efficiency, causing the security-critical propagation path to be mistakenly discarded.
Therefore, in our Call Chain design, the ParaTaintNode incorporates a taint bitmap (taintBitMap) to explicitly record the taint status of the function’s parameters. The bitmap establishes a one-to-one correspondence between each parameter and a binary indicator, expressed as
, where
| Listing 5. False Negatives in SGX Code Due to Pruning. |
![Mathematics 14 01007 i005 Mathematics 14 01007 i005]() |
When the
j-th parameter of a function is tainted, its corresponding bit
is set to 1; otherwise,
is set to 0. As illustrated in
Figure 6, a taintBitMap value of 5—whose binary representation is 0101—indicates that the first and third parameters of the function represented by this ParaTaintNode are tainted.
By using the taintBitMap, we refine the pruning condition such that, in addition to matching function identities, the parameter taint configuration must also be consistent. Formally, let , where and , and . The next node is pruned only if both the function identifiers match and their taintBitMap are identical .
As illustrated in Algorithm 1, if the newly invoked function does not appear in previously generated call nodes (line 3), or it exists but with a different parameter taint state (line 6), then the corresponding ParaTaintNode has not been created yet (line 11). In this case, a new ParaTaintNode is instantiated and inserted into the priority queue
prio_queue. Conversely, if the function already exists in prior call nodes with an identical taint bitmap, it indicates that this ParaTaintNode has already been generated during earlier Call Chain construction (line 9). Therefore, the node does not need to be traversed again and can be safely pruned.
| Algorithm 1 ParaTaintNode Matching Algorithm |
- Input:
Set of all created ParaTaintNode nodes ParaTaintNodeList, address of newly called function address, taint position bitmap of current function taintBitMap - Output:
Node paraTaintNode
- 1:
function getParaTaintNode (ParaTaintNodeList, address, taintBitMap) - 2:
for paraTaintNode ∈ ParaTaintNodeList do - 3:
if paraTaintNode.address ≠ address then ▹ Check function address match - 4:
continue ▹ Different function - 5:
end if - 6:
if paraTaintNode.taintBitMap ≠ taintBitMap then ▹ Compare parameter taint state under same address - 7:
continue ▹ Different taint state - 8:
end if - 9:
return paraTaintNode ▹ Existing node found - 10:
end for - 11:
return null ▹ No matching node exists - 12:
end function
|
4.2.2. Multi-Node Heuristic Priority Search Algorithm
If Call Chain construction relies on depth-first search or purely random traversal, several challenges emerge:
- 1.
The explosive number of Call Chains renders effective path identification infeasible.
- 2.
The chain lengths may grow excessively.
- 3.
Irrelevant nodes that do not contribute to taint propagation may be explored.
- 4.
Numerous redundant paths may be repeatedly visited, ultimately degrading efficiency and potentially preventing the analysis from being completed within a practical time frame.
To address problems 1 and 2, let
denote the call graph, where
V is the set of function nodes and
E is the set of call edges. Let
denote the set of all paths from source nodes to reachable nodes in
G. Under unconstrained depth-first search (DFS) traversal, the total number of paths grows exponentially with the call depth:
In the Formulae (
1),
b is the average branching factor and
d is the maximum call chain depth. When
b and
d are large, the number of paths explodes, making it computationally infeasible to efficiently identify vulnerability-relevant paths through exhaustive traversal.
To address these challenges, we transform the unconstrained search into a heuristic-guided priority search. We define a priority score function for each unvisited ParaTaintNode , where nodes with higher scores are considered more likely to reach sink functions and are therefore expanded first.
We maintain a priority queue
that stores all unvisited ParaTaintNodes ordered by their scores in descending order. Formally, for any two nodes
:
At each iteration, the algorithm dequeues the node with the highest score from for expansion.
The priority score for a ParaTaintNode
V is defined as
represents the branching factor of node V, i.e., the number of potential callee functions reachable from V;
T is the number of tainted parameters, reflecting the taint propagation potential;
is the shortest call chain distance from the source function to V, which penalizes nodes that are too far from the source;
The weights , , and exponent are empirically calibrated on a validation set of SGX applications.
The reward term captures the local taint propagation potential. Intuitively, nodes with more outgoing edges (larger N) offer more opportunities to propagate taint to subsequent functions, while nodes with more tainted parameters (larger T) have higher likelihood of triggering dangerous operations. Hence, nodes with higher reward scores are prioritized for exploration.
The penalty term introduces a non-linear depth penalty. This term discourages the exploration of excessively long call chains by reducing the priority of nodes far from the source, while the exponent ensures that the penalty grows super-linearly with depth. This design preserves the possibility of reaching deeply nested but relevant paths while mitigating the impact of path explosion caused by unbounded depth.
The weighting parameters (, , and ) are determined through a systematic experimental tuning procedure on a validation set of SGX applications. Equal weights are assigned to N and T to reflect their respective contributions to taint propagation potential. The depth penalty weight is set to a relatively low value in order to avoid overly aggressive pruning of deeply nested yet potentially relevant paths, while still discouraging unnecessary exploration. The exponent introduces a mild nonlinear growth in the depth penalty, enabling a balanced trade-off between analysis completeness and efficiency. Our experimental results indicate that moderate variations of these parameters do not significantly affect detection outcomes, suggesting that the scoring function is robust with respect to parameter selection.
Unlike the exhaustive DFS described in Formula (
1), our heuristic-guided priority search selectively expands only a subset of nodes. Let
denote the probability that a node at depth
i is selected for expansion. Due to the depth penalty term
, we have
Importantly, not all nodes at a given depth are selected; each node’s selection probability is independently governed by the priority score. The expected number of explored paths is
Since decays rapidly with depth ( as i increases), the contribution of deeper layers becomes negligible, effectively mitigating the path explosion problem.
In summary, the priority value of a ParaTaintNode is computed by summing the number of functions it can call and the number of its parameter taints, and then subtracting its shortest path length. This design encourages the exploration of new ParaTaintNode nodes, prioritizes nodes more likely to reach sink functions, and mitigates the negative impact of excessively long call chains on analysis efficiency.
As shown in Algorithm 2, we customize a PriorityQueue by defining a dedicated Comparator to order stored ParaTaintNode instances according to a predefined scoring function. Specifically, the compare method of the Comparator interface is overridden (line 2) to establish the node comparison logic. For two ParaTaintNode objects, denoted as node1 and node2, their corresponding scores, score1 and score2, are first computed (line 3 and line 4). The nodes are then sorted in descending order using Double.compare (score2, score1) (line 5), ensuring that the priority queue consistently processes taint nodes with higher scores first.
| Algorithm 2 Priority Queue Comparator |
- Input:
Two ParaTaintNode instances node1 and node2 - Output:
Integer where:
- -
positive value: node2 has higher priority than node1, - -
zero: node1 and node2 have equal priority, - -
negative value: node1 has higher priority than node2
- 1:
Define Comparator <ParaTaintNode> taintNodeComparator: ▹ Custom comparator for ParaTaintNode priority ordering - 2:
function compare(node1, node2) - 3:
score1 ← w1 * node1.taintsNum + w2 * node1.callNum − w3 * Math.pow(node1.depth, alpha) ▹ Calculate priority scores - 4:
score2 ← w1 * node2.taintsNum + w2 * node2.callNum − w3 * Math.pow(node2.depth, alpha) - 5:
return Double.compare(score2, score1) ▹ Descending order (higher score first) - 6:
end function
|
To address problem 3, if all parameters of a newly invoked function are clean, neither this function nor the functions it subsequently calls can contribute to taint propagation. However, conventional call-chain construction algorithms still include such functions, resulting in a large number of ineffective paths. Therefore, when performing taint analysis on the Pcode intermediate representation of the function associated with a ParaTaintNode, we evaluate whether any of its parameters are tainted. If no taint exists—i.e., V(T) = 0 or V(M) = (0, 0, 0, …, 0)—the function and its subsequent paths are excluded from further analysis.
Algorithm 3 illustrates how call chain construction is integrated with intra-function taint propagation to analyze taint flows within the current function. Based on the call opcode in pcode and the taint status of function parameters, the algorithm determines whether a new mapping to a callee function should be established by creating a corresponding ParaTaintNode. Initially, the taint propagation path is initialized (line 2), and the control flow graph of the target function is retrieved (line 3). Using the source information contained in the current ParaTaintNode, the relevant basic blocks are identified and inserted into an initial worklist (line 5). The algorithm then enters the main loop, iterating over all basic blocks in the worklist. Within each basic block, all pcode instructions are processed sequentially (line 11). For each instruction, taint propagation is performed to update both the taint set and the associated propagation path (line 12). When a call opcode is encountered, a critical decision is made (line 13). If any input varnode of the call instruction is tainted, the target function address is extracted, and a taint bitmap for the callee’s parameters is computed. A new ParaTaintNode is then created and inserted into the priority queue as well as the global node list (line 16). This newly created node is linked to the current paraTaintNode, thereby extending the call chain while preserving the taint propagation path. If the new node corresponds to a sink function, such as
strcpy,
memcpy, or
snprintf, which are representative data-copy-related functions, it is added to the
sinkParaTaintNodeList for subsequent backward analysis (line 17). After all pcode instructions within a basic block have been processed, the resulting taint set is associated with that block (line 22). The algorithm continues processing remaining basic blocks in the worklist until the list is exhausted, completing intra-function taint propagation for the current function.
| Algorithm 3 Taint Propagation Analysis Function Algorithm |
- Input:
Taint analysis function func, call chain node paraTaintNode, dangerous function node set sinkParaTaintNodeList, priority queue priority_queue, set of all created nodes ParaTaintNodeList - Output:
No return value
- 1:
function funcTaintsFlow(func, paraTaintNode, sinkParaTaintNodeList, priority_queue, ParaTaintNodeList) - 2:
path ← initPath() ▹ Initialize taint propagation path - 3:
cfg ← getCfg(func) ▹ Extract control flow graph - 4:
sourceBlock ← getSourceBlock(cfg, getSource(paraTaintNode)) - 5:
basicBlockList ← InitWorkList(sourceBlock) ▹ Initialize worklist with source blocks - 6:
while basicBlockList ≠ null do - 7:
basicBlock ← getBasicBlock(basicBlockList) - 8:
addSuccBasicBlockToWorklist(basicBlockList, basicBlock) ▹ Add successors to worklist - 9:
taintSet ← getTaintSetByPreBasicBlock(basicBlock) ▹ Merge taint sets from predecessors - 10:
pcodes ← getPcodeByBasicBlock(basicBlock) ▹ Get intermediate representations pcode - 11:
for pcode ∈ pcodes do - 12:
taintFlow(pcode, taintSet, path) ▹ Propagate taints and update path - 13:
if getOpcode(pcode) CALL and inVarnode ∈ taintSet then - 14:
targetFuncAddr ← getAddress(pcode) ▹ Extract callee address - 15:
taintBitMap ← getTaintBitMap(pcode, taintSet) - 16:
newParaTaintNode ← AddNewParaTaintNode(priority_queue, ParaTaintNodeList, paraTaintNode, targetFuncAddr, taintBitMap, path) ▹ Create new call chain node and establish link - 17:
if isSinkFunction(newParaTaintNode) then - 18:
addSinkParaTaintNodeList(newParaTaintNode, sinkParaTaintNodeList) ▹ Add sink nodes corresponding to data-copy-related sink functions (e.g., strcpy, memcpy, snprintf) - 19:
end if - 20:
end if - 21:
end for - 22:
basicBlock ← AddtaintSet(basicBlock, taintSet) ▹ Attach final taint set to basic block - 23:
end while - 24:
end function
|
To address problem 4, when loops or mutually recursive calls occur within the program, our modeling strategy—where each distinct ParaTaintNode is uniquely defined by its function and associated parameter taint state—allows us to enforce a single-visit policy for each node. This effectively prevents redundant exploration of call paths arising from cyclic or mutually recursive structures.
Accordingly, we maintain a ParaTaintNodeList that records all ParaTaintNode instances created during call chain construction. Before inserting a new ParaTaintNode into the prio_queue, we first check whether it already exists in this list. If so, the node is not re-created or re-enqueued. In this way, ParaTaintNodeList guarantees that each ParaTaintNode is processed at most once.
Algorithm 4 illustrates the construction procedure of a ParaTaintNode, which enforces that each node is visited at most once. After taint propagation identifies the functions invoked by the current function, the getParaTaintNode method determines—based on the callee’s address and its parameter taint state—whether a corresponding ParaTaintNode already exists in the previously constructed Call Chain (line 3). If such a node is found, it is excluded from reinsertion into the priority queue
prio_queue; instead, only its predecessor reference (line 8) and shortest path distance (line 7) are updated. Conversely, if no existing node matches the current function invocation, a new ParaTaintNode is initialized (line 4) and inserted into
prio_queue for subsequent exploration (line 5).
| Algorithm 4 Adding New ParaTaint Node Algorithm |
- Input:
Priority queue priority_queue, set of all created nodes ParaTaintNodeList, predecessor node preParaTaintNode, current function address targetFuncAddr, parameter taint bitmap taintBitMap, taint propagation path from predecessor node to current node path - Output:
Call chain node newParaTaintNode
- 1:
function AddNewParaTaintNode(priority_queue, ParaTaintNodeList, preParaTaintNode, targetFuncAddr, taintBitMap, path) - 2:
newParaTaintNode ← getParaTaintNode(ParaTaintNodeList, targetFuncAddr, taintBitMap) ▹ Check if node already exists - 3:
if newParaTaintNode null then - 4:
newParaTaintNode ← new ParaTaintNode(getTaintsNum(taintBitMap), preParaTaintNode.depth , getCallNum(address), taintBitMap, address) ▹ Initialize new node(prevents redundant analysis) - 5:
addParaTaintNode(priority_queue, ParaTaintNodeList, newParaTaintNode)▹ Add to priority_queue and ParaTaintNodeList - 6:
end if - 7:
newParaTaintNode.depth ← min(preParaTaintNode.depth , paraTaintNode.depth) ▹ Update minimal call chain depth - 8:
addPreParaTaintNode(newParaTaintNode, preParaTaintNode, path)▹ Establish predecessor link with propagation path - 9:
return newParaTaintNode - 10:
end function
|
4.2.3. Path Backtracking
After the call chain construction is completed, we backtrack from the ParaTaintNode associated with the dangerous function to the ParaTaintNode corresponding to the source function. Since each ParaTaintNode records the taint propagation segment from its predecessor to itself, concatenating these segments along the same chain yields the final taint propagation path.
In the call chain backtracking process, considering only inter-function invocation relationships without incorporating taint flow semantics may incorrectly classify non-taint-related paths as dangerous, thereby producing false positives. As illustrated in Listing 6, the actual vulnerable paths are ecall_process_network → internal_function_B → ocall_write_to_network and ecall_process_log → internal_function_B → ocall_write_to_log. However, if the Call Chain is built solely based on function invocation relationships, the case depicted in
Figure 7 can arise. During call chain backtracking, the absence of parameter-level taint information prevents us from distinguishing the true data-dependent path, potentially leading to an incorrect interpretation such as ecall_process_network → internal_function_B → ocall_write_to_log, which ultimately causes false positive reports.
By adopting our call chain construction method that treats the "Function and Parameter Taint State" as the fundamental analysis unit, these false positives are effectively eliminated. As illustrated in
Figure 8, the backtracking process ultimately yields two valid taint propagation paths, both of which actually exist in the program.
Moreover, after constructing the Call Chain using ’Function and Parameter Taint State’ as the basic abstraction unit, some redundant paths may still remain in the resulting chain. These irrelevant chains introduce unnecessary overhead and therefore degrade efficiency during the subsequent backtracking phase.
| Listing 6. False Positive Example in Path Backtracking. |
![Mathematics 14 01007 i006 Mathematics 14 01007 i006]() |
To eliminate redundant backtracking paths, we formalize a taint-aware pruning strategy based on subset comparison of taint bitmaps.
For two ParaTaintNodes,
and
, with the same function identifier (
), we say that
is
taint-subsumed by
, denoted as
, if and only if
During backtracking from sink to source, let
be the current path under exploration. Let
be the current node. If there exists a previously visited node
such that
then further backtracking from
is pruned. This process is illustrated in Algorithm 5.
| Algorithm 5 Pruning Condition Check |
- Input:
Previously visited node kParaTaintNode, current node currentParaTaintNode - Output:
Boolean indicating whether pruning is applicable
- 1:
function AddNewParaTaintNode(kParaTaintNode, currentParaTaintNode) - 2:
if then - 3:
return True ▹ Pruning applicable - 4:
else - 5:
return False ▹ Continue forward tracing - 6:
end if - 7:
end function
|
4.2.4. Precision–Efficiency Trade-Off
Static analysis tools for vulnerability detection often face a fundamental trade-off between precision and efficiency. Highly precise analysis typically requires fine-grained modeling of program states, which can significantly increase the analysis space and computational overhead. Conversely, aggressive pruning and coarse-grained modeling may improve efficiency but often introduce false positives or false negatives.
ParaTaintGX is designed to balance this trade-off through a combination of fine-grained taint modeling and heuristic exploration strategies.
ParaTaintGX improves detection precision by introducing the ParaTaintNode model, which represents a function together with its parameter taint state. Compared with traditional function-level call graph nodes, this representation distinguishes different invocation contexts based on the differences in parameter taint. As a result, ParaTaintGX can avoid incorrect pruning of vulnerability-relevant paths and significantly reduce false negatives during call-chain construction.
However, parameter-level taint modeling may increase the number of potential analysis states. To mitigate this overhead and maintain scalability, ParaTaintGX integrates two efficiency-oriented mechanisms.
First, a multi-node heuristic priority search algorithm is adopted to guide the exploration of the call graph. By prioritizing nodes with larger taint propagation potential and shorter distances to the source, the algorithm focuses on paths that are more likely to reach vulnerability sinks, thereby reducing unnecessary exploration of irrelevant call chains.
Second, ParaTaintGX introduces a taint-state-aware pruning strategy during backward tracing. If the taint state of a newly visited node contains that of a previously visited node, further exploration of that path is pruned. This strategy effectively eliminates redundant analysis paths while preserving vulnerability-relevant propagation chains.
Through the combination of parameter-sensitive taint modeling, heuristic exploration, and taint-aware pruning, ParaTaintGX achieves a practical balance between analysis precision and computational efficiency.