Skip to content

Commit

Permalink
Merge branch 'ak/fixpointv2' into ak/fixpointv2-unreachableeog
Browse files Browse the repository at this point in the history
  • Loading branch information
KuechA authored Nov 11, 2024
2 parents 2d36149 + 9b583d5 commit e4bfb50
Show file tree
Hide file tree
Showing 11 changed files with 475 additions and 227 deletions.
261 changes: 143 additions & 118 deletions cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -200,50 +200,115 @@ class FulfilledAndFailedPaths(val fulfilled: List<List<Node>>, val failed: List<
* but not mandatory**. If the list "failed" is empty, the data flow is mandatory.
*/
fun Node.followPrevFullDFGEdgesUntilHit(predicate: (Node) -> Boolean): FulfilledAndFailedPaths {
val fulfilledPaths = mutableListOf<List<Node>>()
val failedPaths = mutableListOf<List<Node>>()
val worklist = mutableListOf<List<Node>>()
worklist.add(listOf(this))
return followXUntilHit(
x = { currentNode -> currentNode.prevFullDFG },
collectFailedPaths = true,
findAllPossiblePaths = true,
predicate = predicate
)
}

while (worklist.isNotEmpty()) {
val currentPath = worklist.removeFirst()
if (currentPath.last().prevFullDFG.isEmpty()) {
// No further nodes in the path and the path criteria are not satisfied.
failedPaths.add(currentPath)
continue
fun Node.collectAllPrevCDGPaths(interproceduralAnalysis: Boolean): List<List<Node>> {
// We make everything fail to reach the end of the CDG. Then, we use the stuff collected in the
// failed paths (everything)
return this.followPrevCDGUntilHit(
collectFailedPaths = true,
findAllPossiblePaths = true,
interproceduralAnalysis = interproceduralAnalysis
) {
false
}
.failed
}

for (prev in currentPath.last().prevFullDFG) {
// Copy the path for each outgoing DFG edge and add the prev node
val nextPath = mutableListOf<Node>()
nextPath.addAll(currentPath)
nextPath.add(prev)

if (predicate(prev)) {
fulfilledPaths.add(nextPath)
continue // Don't add this path anymore. The requirement is satisfied.
}
// The prev node is new in the current path (i.e., there's no loop), so we add the path
// with the next step to the worklist.
if (!currentPath.contains(prev)) {
worklist.add(nextPath)
}
fun Node.collectAllNextCDGPaths(interproceduralAnalysis: Boolean): List<List<Node>> {
// We make everything fail to reach the end of the CDG. Then, we use the stuff collected in the
// failed paths (everything)
return this.followNextCDGUntilHit(
collectFailedPaths = true,
findAllPossiblePaths = true,
interproceduralAnalysis = interproceduralAnalysis
) {
false
}
}

return FulfilledAndFailedPaths(fulfilledPaths, failedPaths)
.failed
}

/**
* Returns an instance of [FulfilledAndFailedPaths] where [FulfilledAndFailedPaths.fulfilled]
* contains all possible shortest data flow paths (with [FullDataflowGranularity]) between the
* starting node [this] and the end node fulfilling [predicate]. The paths are represented as lists
* of nodes. Paths which do not end at such a node are included in [FulfilledAndFailedPaths.failed].
* contains all possible shortest data flow paths (with [ControlDependence]) between the starting
* node [this] and the end node fulfilling [predicate]. The paths are represented as lists of nodes.
* Paths which do not end at such a node are included in [FulfilledAndFailedPaths.failed].
*
* Hence, if "fulfilled" is a non-empty list, a data flow from [this] to such a node is **possible
* but not mandatory**. If the list "failed" is empty, the data flow is mandatory.
*/
fun Node.followNextFullDFGEdgesUntilHit(
fun Node.followNextCDGUntilHit(
collectFailedPaths: Boolean = true,
findAllPossiblePaths: Boolean = true,
interproceduralAnalysis: Boolean = false,
predicate: (Node) -> Boolean
): FulfilledAndFailedPaths {
return followXUntilHit(
x = { currentNode ->
val nextNodes = currentNode.nextCDG.toMutableList()
if (interproceduralAnalysis) {
nextNodes.addAll((currentNode as? CallExpression)?.calls ?: listOf())
}
nextNodes
},
collectFailedPaths = collectFailedPaths,
findAllPossiblePaths = findAllPossiblePaths,
predicate = predicate
)
}

/**
* Returns an instance of [FulfilledAndFailedPaths] where [FulfilledAndFailedPaths.fulfilled]
* contains all possible shortest data flow paths (with [ControlDependence]) between the starting
* node [this] and the end node fulfilling [predicate] (backwards analysis). The paths are
* represented as lists of nodes. Paths which do not end at such a node are included in
* [FulfilledAndFailedPaths.failed].
*
* Hence, if "fulfilled" is a non-empty list, a CDG path from [this] to such a node is **possible
* but not mandatory**. If the list "failed" is empty, the data flow is mandatory.
*/
fun Node.followPrevCDGUntilHit(
collectFailedPaths: Boolean = true,
findAllPossiblePaths: Boolean = true,
interproceduralAnalysis: Boolean = false,
predicate: (Node) -> Boolean
): FulfilledAndFailedPaths {
return followXUntilHit(
x = { currentNode ->
val nextNodes = currentNode.prevCDG.toMutableList()
if (interproceduralAnalysis) {
nextNodes.addAll(
(currentNode as? FunctionDeclaration)?.usages?.mapNotNull {
it.astParent as? CallExpression
} ?: listOf()
)
}
nextNodes
},
collectFailedPaths = collectFailedPaths,
findAllPossiblePaths = findAllPossiblePaths,
predicate = predicate
)
}

/**
* Returns an instance of [FulfilledAndFailedPaths] where [FulfilledAndFailedPaths.fulfilled]
* contains all possible shortest data flow paths (with [x] specifying how to fetch more nodes)
* between the starting node [this] and the end node fulfilling [predicate] (backwards analysis).
* The paths are represented as lists of nodes. Paths which do not end at such a node are included
* in [FulfilledAndFailedPaths.failed].
*
* Hence, if "fulfilled" is a non-empty list, a path from [this] to such a node is **possible but
* not mandatory**. If the list "failed" is empty, the path is mandatory.
*/
fun Node.followXUntilHit(
x: (Node) -> List<Node>,
collectFailedPaths: Boolean = true,
findAllPossiblePaths: Boolean = true,
predicate: (Node) -> Boolean
Expand All @@ -264,15 +329,15 @@ fun Node.followNextFullDFGEdgesUntilHit(
worklist.remove(currentPath)
val currentNode = currentPath.last()
alreadySeenNodes.add(currentNode)
// The last node of the path is where we continue. We get all of its outgoing DFG edges and
// The last node of the path is where we continue. We get all of its outgoing CDG edges and
// follow them
if (currentNode.nextFullDFG.isEmpty()) {
// No further nodes in the path and the path criteria are not satisfied.
if (collectFailedPaths) failedPaths.add(currentPath)
}
var nextNodes = x(currentNode)

// No further nodes in the path and the path criteria are not satisfied.
if (nextNodes.isEmpty() && collectFailedPaths) failedPaths.add(currentPath)

for (next in currentNode.nextFullDFG) {
// Copy the path for each outgoing DFG edge and add the next node
for (next in nextNodes) {
// Copy the path for each outgoing CDG edge and add the next node
val nextPath = currentPath.toMutableList()
nextPath.add(next)
if (predicate(next)) {
Expand All @@ -296,6 +361,28 @@ fun Node.followNextFullDFGEdgesUntilHit(
return FulfilledAndFailedPaths(fulfilledPaths, failedPaths)
}

/**
* Returns an instance of [FulfilledAndFailedPaths] where [FulfilledAndFailedPaths.fulfilled]
* contains all possible shortest data flow paths (with [FullDataflowGranularity]) between the
* starting node [this] and the end node fulfilling [predicate]. The paths are represented as lists
* of nodes. Paths which do not end at such a node are included in [FulfilledAndFailedPaths.failed].
*
* Hence, if "fulfilled" is a non-empty list, a data flow from [this] to such a node is **possible
* but not mandatory**. If the list "failed" is empty, the data flow is mandatory.
*/
fun Node.followNextFullDFGEdgesUntilHit(
collectFailedPaths: Boolean = true,
findAllPossiblePaths: Boolean = true,
predicate: (Node) -> Boolean
): FulfilledAndFailedPaths {
return followXUntilHit(
x = { currentNode -> currentNode.nextFullDFG },
collectFailedPaths = collectFailedPaths,
findAllPossiblePaths = findAllPossiblePaths,
predicate = predicate
)
}

/**
* Returns an instance of [FulfilledAndFailedPaths] where [FulfilledAndFailedPaths.fulfilled]
* contains all possible shortest evaluation paths between the starting node [this] and the end node
Expand All @@ -307,45 +394,14 @@ fun Node.followNextFullDFGEdgesUntilHit(
* such a statement is always executed.
*/
fun Node.followNextEOGEdgesUntilHit(predicate: (Node) -> Boolean): FulfilledAndFailedPaths {
// Looks complicated but at least it's not recursive...
// result: List of paths (between from and to)
val fulfilledPaths = mutableListOf<List<Node>>()
// failedPaths: All the paths which do not satisfy "predicate"
val failedPaths = mutableListOf<List<Node>>()
// The list of paths where we're not done yet.
val worklist = mutableListOf<List<Node>>()
worklist.add(listOf(this)) // We start only with the "from" node (=this)

while (worklist.isNotEmpty()) {
val currentPath = worklist.removeFirst()
// The last node of the path is where we continue. We get all of its outgoing DFG edges and
// follow them
if (currentPath.last().nextEOGEdges.none { it.unreachable != true }) {
// No further nodes in the path and the path criteria are not satisfied.
failedPaths.add(currentPath)
continue // Don't add this path anymore. The requirement is satisfied.
}

for (next in
currentPath.last().nextEOGEdges.filter { it.unreachable != true }.map { it.end }) {
// Copy the path for each outgoing DFG edge and add the next node
val nextPath = mutableListOf<Node>()
nextPath.addAll(currentPath)
nextPath.add(next)
if (predicate(next)) {
// We ended up in the node "to", so we're done. Add the path to the results.
fulfilledPaths.add(nextPath)
continue // Don't add this path anymore. The requirement is satisfied.
}
// The next node is new in the current path (i.e., there's no loop), so we add the path
// with the next step to the worklist.
if (!currentPath.contains(next)) {
worklist.add(nextPath)
}
}
}

return FulfilledAndFailedPaths(fulfilledPaths, failedPaths)
return followXUntilHit(
x = { currentNode ->
currentNode.nextEOGEdges.filter { it.unreachable != true }.map { it.end }
},
collectFailedPaths = true,
findAllPossiblePaths = true,
predicate = predicate
)
}

/**
Expand All @@ -359,45 +415,14 @@ fun Node.followNextEOGEdgesUntilHit(predicate: (Node) -> Boolean): FulfilledAndF
* such a statement is always executed.
*/
fun Node.followPrevEOGEdgesUntilHit(predicate: (Node) -> Boolean): FulfilledAndFailedPaths {
// Looks complicated but at least it's not recursive...
// result: List of paths (between from and to)
val fulfilledPaths = mutableListOf<List<Node>>()
// failedPaths: All the paths which do not satisfy "predicate"
val failedPaths = mutableListOf<List<Node>>()
// The list of paths where we're not done yet.
val worklist = mutableListOf<List<Node>>()
worklist.add(listOf(this)) // We start only with the "from" node (=this)

while (worklist.isNotEmpty()) {
val currentPath = worklist.removeFirst()
// The last node of the path is where we continue. We get all of its outgoing DFG edges and
// follow them
if (currentPath.last().prevEOGEdges.none { it.unreachable != true }) {
// No further nodes in the path and the path criteria are not satisfied.
failedPaths.add(currentPath)
continue // Don't add this path anymore. The requirement is satisfied.
}

for (next in
currentPath.last().prevEOGEdges.filter { it.unreachable != true }.map { it.start }) {
// Copy the path for each outgoing DFG edge and add the next node
val nextPath = mutableListOf<Node>()
nextPath.addAll(currentPath)
nextPath.add(next)
if (predicate(next)) {
// We ended up in the node "to", so we're done. Add the path to the results.
fulfilledPaths.add(nextPath)
continue // Don't add this path anymore. The requirement is satisfied.
}
// The next node is new in the current path (i.e., there's no loop), so we add the path
// with the next step to the worklist.
if (!currentPath.contains(next)) {
worklist.add(nextPath)
}
}
}

return FulfilledAndFailedPaths(fulfilledPaths, failedPaths)
return followXUntilHit(
x = { currentNode ->
currentNode.prevEOGEdges.filter { it.unreachable != true }.map { it.start }
},
collectFailedPaths = true,
findAllPossiblePaths = true,
predicate = predicate
)
}

/**
Expand Down Expand Up @@ -634,7 +659,7 @@ fun Node.firstParentOrNull(predicate: (Node) -> Boolean): Node? {
return node
}

// go up-wards in the ast tree
// go upwards in the ast tree
node = node.astParent
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import de.fraunhofer.aisec.cpg.graph.NodeBuilder.LOGGER
import de.fraunhofer.aisec.cpg.graph.NodeBuilder.log
import de.fraunhofer.aisec.cpg.graph.scopes.Scope
import de.fraunhofer.aisec.cpg.graph.statements.expressions.*
import de.fraunhofer.aisec.cpg.graph.types.*
import de.fraunhofer.aisec.cpg.helpers.getCodeOfSubregion
import de.fraunhofer.aisec.cpg.passes.inference.IsImplicitProvider
import de.fraunhofer.aisec.cpg.passes.inference.IsInferredProvider
Expand Down Expand Up @@ -299,10 +298,16 @@ fun <T : Node, AstNode> T.codeAndLocationFromOtherRawNode(rawNode: AstNode?): T
* code is extracted from the parent node to catch separators and auxiliary syntactic elements that
* are between the child nodes.
*
* @param parentNode Used to extract the code for this node
* @param parentNode Used to extract the code for this node.
* @param newLineType The char(s) used to describe a new line, usually either "\n" or "\r\n". This
* is needed because the location block spanning the children usually comprises more than one
* line.
*/
context(CodeAndLocationProvider<AstNode>)
fun <T : Node, AstNode> T.codeAndLocationFromChildren(parentNode: AstNode): T {
fun <T : Node, AstNode> T.codeAndLocationFromChildren(
parentNode: AstNode,
lineBreakSequence: CharSequence = "\n"
): T {
var first: Node? = null
var last: Node? = null

Expand Down Expand Up @@ -358,7 +363,7 @@ fun <T : Node, AstNode> T.codeAndLocationFromChildren(parentNode: AstNode): T {
val parentRegion = this@CodeAndLocationProvider.locationOf(parentNode)?.region
if (parentCode != null && parentRegion != null) {
// If the parent has code and region the new region is used to extract the code
this.code = getCodeOfSubregion(parentCode, parentRegion, newRegion)
this.code = getCodeOfSubregion(parentCode, parentRegion, newRegion, lineBreakSequence)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ import de.fraunhofer.aisec.cpg.graph.edges.collections.EdgeSet
import de.fraunhofer.aisec.cpg.graph.edges.collections.MirroredEdgeCollection
import de.fraunhofer.aisec.cpg.graph.statements.expressions.*
import de.fraunhofer.aisec.cpg.graph.types.HasType
import de.fraunhofer.aisec.cpg.helpers.neo4j.DataflowGranularityConverter
import kotlin.reflect.KProperty
import org.neo4j.ogm.annotation.*
import org.neo4j.ogm.annotation.typeconversion.Convert

/**
* The granularity of the data-flow, e.g., whether the flow contains the whole object, or just a
Expand Down Expand Up @@ -87,7 +89,9 @@ open class Dataflow(
start: Node,
end: Node,
/** The granularity of this dataflow. */
@Transient @JsonIgnore var granularity: Granularity = default()
@Convert(DataflowGranularityConverter::class)
@JsonIgnore
var granularity: Granularity = default()
) : Edge<Node>(start, end) {
override val label: String = "DFG"

Expand Down
Loading

0 comments on commit e4bfb50

Please sign in to comment.