Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Following EOG edges interprocedurally with stack #1999

Merged
merged 19 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,7 @@ class ScopeManager : ScopeProvider {

/** The current block, according to the scope that is currently active. */
val currentBlock: Block?
get() =
currentScope?.astNode as? Block
?: currentScope?.astNode?.firstParentOrNull { it is Block } as? Block
get() = currentScope?.astNode as? Block ?: currentScope?.astNode?.firstParentOrNull<Block>()

/**
* The current method in the active scope tree, this ensures that 'this' keywords are mapped
Expand Down
108 changes: 93 additions & 15 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 @@ -34,6 +34,7 @@
import de.fraunhofer.aisec.cpg.graph.statements.expressions.*
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block
import de.fraunhofer.aisec.cpg.helpers.SubgraphWalker
import kotlin.collections.filter
import kotlin.collections.firstOrNull
import kotlin.math.absoluteValue

Expand Down Expand Up @@ -285,9 +286,7 @@
// from).
// We try to pop from the stack and only select the elements with the
// matching index.
ctx.indexStack.popIfOnTop(
it.granularity as IndexedDataflowGranularity
) == true
ctx.indexStack.popIfOnTop(it.granularity as IndexedDataflowGranularity)
} else {
true
}
Expand Down Expand Up @@ -328,7 +327,7 @@
val callStack: SimpleStack<CallExpression> = SimpleStack(),
) {
fun clone(): Context {
return Context(indexStack.clone(), callStack.clone())
return Context(indexStack = indexStack.clone(), callStack = callStack.clone())
}
}

Expand Down Expand Up @@ -361,6 +360,9 @@
return false
}

/** Pops the top element from the stack. */
fun pop(): T = deque.removeFirst()

/** Clones the stack. */
fun clone(): SimpleStack<T> {
return SimpleStack<T>().apply { deque.addAll(this@SimpleStack.deque) }
Expand All @@ -387,10 +389,14 @@
* Iterates the next EOG edges until there are no more edges available (or until a loop is
* detected). Returns a list of possible paths (each path is represented by a list of nodes).
*/
fun Node.collectAllNextEOGPaths(): List<List<Node>> {
fun Node.collectAllNextEOGPaths(interproceduralAnalysis: Boolean = true): List<List<Node>> {

Check warning on line 392 in cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt

View check run for this annotation

Codecov / codecov/patch

cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt#L392

Added line #L392 was not covered by tests
// We make everything fail to reach the end of the CDG. Then, we use the stuff collected in the
// failed paths (everything)
return this.followNextEOGEdgesUntilHit(collectFailedPaths = true, findAllPossiblePaths = true) {
return this.followNextEOGEdgesUntilHit(
collectFailedPaths = true,
findAllPossiblePaths = true,
interproceduralAnalysis = interproceduralAnalysis,
) {
false
}
.failed
Expand Down Expand Up @@ -658,7 +664,7 @@
(next !in alreadySeenNodes && worklist.none { next in it.first }))
) {
val newContext =
if (nextPath.size > 1) {
if (nextNodes.size > 1) {
currentContext.clone()
} else {
currentContext
Expand Down Expand Up @@ -799,18 +805,60 @@
fun Node.followNextEOGEdgesUntilHit(
collectFailedPaths: Boolean = true,
findAllPossiblePaths: Boolean = true,
interproceduralAnalysis: Boolean = true,
predicate: (Node) -> Boolean,
): FulfilledAndFailedPaths {
return followXUntilHit(
x = { currentNode, _, _ ->
currentNode.nextEOGEdges.filter { it.unreachable != true }.map { it.end }
x = { currentNode, ctx, _ ->
if (
interproceduralAnalysis &&
currentNode is CallExpression &&
currentNode.invokes.isNotEmpty()
) {
// We follow the invokes edges and push the call expression on the call stack, so we
// can jump back here after processing the function.
ctx.callStack.push(currentNode)
currentNode.invokes.flatMap { it.eogStarters }
} else if (
interproceduralAnalysis &&
(currentNode is ReturnStatement || currentNode.nextEOG.isEmpty())
) {
if (ctx.callStack.isEmpty()) {
(currentNode as? FunctionDeclaration
?: currentNode.firstParentOrNull<FunctionDeclaration>())
?.calledBy
?.flatMap { it.nextEOG } ?: setOf()
} else {
ctx.callStack.pop().nextEOG
}
} else {
currentNode.nextEOGEdges.filter { it.unreachable != true }.map { it.end }
}
},
collectFailedPaths = collectFailedPaths,
findAllPossiblePaths = findAllPossiblePaths,
predicate = predicate,
)
}

/**
* Returns the a [Collection] of last nodes in the EOG of this [FunctionDeclaration]. If there's no
* body, it will return a list of this function declaration.
*/
val FunctionDeclaration.lastEOGNode: Collection<Node>
get() {
val lastEOG = collectAllNextEOGPaths(false).flatMap { it.last().prevEOGEdges }
return if (lastEOG.isEmpty()) {
// In some cases, we do not have a body, so we have to jump directly to the
// function declaration.
listOf(this)
} else lastEOG.filter { it.unreachable != true }.map { it.start }
}

/** Returns only potentially reachable previous EOG edges. */
val Node.reachablePrevEOG: Collection<Node>
get() = this.prevEOGEdges.filter { it.unreachable != true }.map { it.start }

/**
* Returns an instance of [FulfilledAndFailedPaths] where [FulfilledAndFailedPaths.fulfilled]
* contains all possible shortest evaluation paths between the end node [this] and the start node
Expand All @@ -824,11 +872,39 @@
fun Node.followPrevEOGEdgesUntilHit(
collectFailedPaths: Boolean = true,
findAllPossiblePaths: Boolean = true,
interproceduralAnalysis: Boolean = true,
predicate: (Node) -> Boolean,
): FulfilledAndFailedPaths {

return followXUntilHit(
x = { currentNode, _, _ ->
currentNode.prevEOGEdges.filter { it.unreachable != true }.map { it.start }
x = { currentNode, ctx, _ ->
when {
interproceduralAnalysis &&
currentNode is FunctionDeclaration &&
ctx.callStack.isEmpty() -> {
// We're at the beginning of a function. If the stack is empty, we jump to
// all calls of this function.
currentNode.calledBy.flatMap { it.reachablePrevEOG }
}
interproceduralAnalysis && currentNode is FunctionDeclaration -> {
// We're at the beginning of a function. If there's something on the stack,
// we ended up here by following the respective call expression, and we jump
// back there.
ctx.callStack.pop().reachablePrevEOG
}
interproceduralAnalysis &&
currentNode is CallExpression &&
currentNode.invokes.isNotEmpty() -> {
// We're in the call expression. Push it on the stack, go to all last EOG
// nodes in the functions which are invoked and continue there.
ctx.callStack.push(currentNode)

currentNode.invokes.flatMap { it.lastEOGNode }
}
else -> {
currentNode.reachablePrevEOG
}
}
},
collectFailedPaths = collectFailedPaths,
findAllPossiblePaths = findAllPossiblePaths,
Expand Down Expand Up @@ -1067,13 +1143,15 @@
*
* @param predicate the search predicate
*/
fun Node.firstParentOrNull(predicate: (Node) -> Boolean): Node? {
inline fun <reified T : Node> Node.firstParentOrNull(

Check warning on line 1146 in cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt

View check run for this annotation

Codecov / codecov/patch

cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt#L1146

Added line #L1146 was not covered by tests
noinline predicate: ((T) -> Boolean)? = null
): T? {

// start at searchNodes parent
var node: Node? = this.astParent
var node = this.astParent

while (node != null) {
if (predicate(node)) {
if (node is T && (predicate == null || predicate(node))) {
return node
}

Expand Down Expand Up @@ -1272,7 +1350,7 @@
/** Returns the [TranslationUnitDeclaration] where this node is located in. */
val Node.translationUnit: TranslationUnitDeclaration?
get() {
return firstParentOrNull { it is TranslationUnitDeclaration } as? TranslationUnitDeclaration
return firstParentOrNull<TranslationUnitDeclaration>()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,9 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa
}
// Forwards all open and uncaught throwing nodes to the outer scope that may handle them
val outerCatchingNode =
node.firstParentOrNull { parent -> parent is TryStatement || parent is LoopStatement }
node.firstParentOrNull<Node> { parent ->
parent is TryStatement || parent is LoopStatement
}
if (outerCatchingNode != null) {
// Forwarding is done by merging the currently associated throws to a type with the new
// throws based on their type
Expand Down Expand Up @@ -1311,7 +1313,7 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa
if (throwType != null) {
// Here, we identify the encapsulating ast node that can handle or relay a throw
val handlingOrRelayingParent =
throwExpression.firstParentOrNull { parent ->
throwExpression.firstParentOrNull<Node> { parent ->
parent is TryStatement || parent is FunctionDeclaration
}
if (handlingOrRelayingParent != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,8 +597,7 @@ class Inference internal constructor(val start: Node, override val ctx: Translat
}
is ReturnStatement -> {
// If this is part of a return statement, we can take the return type
val func =
hint.firstParentOrNull { it is FunctionDeclaration } as? FunctionDeclaration
val func = hint.firstParentOrNull<FunctionDeclaration>()
val returnTypes = func?.returnTypes

return if (returnTypes != null && returnTypes.size > 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,15 +398,33 @@ class ShortcutsTest {
val ifCondition = ifStatement.condition
assertIs<BinaryOperator>(ifCondition)

val paramPassed =
ifCondition.followNextEOGEdgesUntilHit {
// There are the following paths:
// - the else branch (which fulfills the requirement)
// - the then/then (fails)
// - the then/else (fails)
val paramPassedIntraproceduralOnly =
ifCondition.followNextEOGEdgesUntilHit(interproceduralAnalysis = false) {
it is AssignExpression &&
it.operatorCode == "=" &&
(it.rhs.first() as? Reference)?.refersTo ==
(ifCondition.lhs as? Reference)?.refersTo
}
assertEquals(1, paramPassed.fulfilled.size)
assertEquals(2, paramPassed.failed.size)
assertEquals(1, paramPassedIntraproceduralOnly.fulfilled.size)
assertEquals(2, paramPassedIntraproceduralOnly.failed.size)

// There are the following paths:
// - the else branch (which fulfills the requirement)
// - the then/then and 3 paths when we enter magic2 through this path (=> 3 fails)
// - the then/else and 3 paths when we enter magic2 through this path (=> 3 fails)
val paramPassedInterprocedural =
ifCondition.followNextEOGEdgesUntilHit(interproceduralAnalysis = true) {
it is AssignExpression &&
it.operatorCode == "=" &&
(it.rhs.first() as? Reference)?.refersTo ==
(ifCondition.lhs as Reference).refersTo
}
assertEquals(1, paramPassedInterprocedural.fulfilled.size)
assertEquals(6, paramPassedInterprocedural.failed.size)
}

@Test
Expand Down
Loading