diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementBuilder.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementBuilder.kt index b01b6ce5c13..7abfc4a9e84 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementBuilder.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementBuilder.kt @@ -356,3 +356,18 @@ fun MetadataProvider.newLookupScopeStatement( log(node) return node } + +/** + * Creates a new [ThrowStatement]. The [MetadataProvider] receiver will be used to fill different + * meta-data using [Node.applyMetadata]. Calling this extension function outside of Kotlin requires + * an appropriate [MetadataProvider], such as a [LanguageFrontend] as an additional prepended + * argument. + */ +@JvmOverloads +fun MetadataProvider.newThrowStatement(rawNode: Any? = null): ThrowStatement { + val node = ThrowStatement() + node.applyMetadata(this, EMPTY_NAME, rawNode, true) + + log(node) + return node +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt index 5cd242aa0c1..1ea29ec5895 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt @@ -1400,6 +1400,7 @@ infix fun Expression.assignAsExpr(rhs: Expression): AssignExpression { return node } + /** * Creates a new [AssignExpression] with a `=` [AssignExpression.operatorCode] in the Fluent Node * DSL and adds it to the nearest enclosing [StatementHolder]. @@ -1414,6 +1415,23 @@ infix fun Expression.assignAsExpr(rhs: AssignExpression.() -> Unit): AssignExpre return node } +/** + * Creates a new [ThrowStatement] in the Fluent Node DSL and adds it to the nearest enclosing + * [StatementHolder]. + */ +context(LanguageFrontend<*, *>, Holder) +infix fun Expression.`throw`(init: (ThrowStatement.() -> Unit)?): ThrowStatement { + val node = (this@LanguageFrontend).newThrowStatement() + if (init != null) init(node) + + val holder = this@Holder + if (holder is StatementHolder) { + holder += node + } + + return node +} + /** Creates a new [Type] with the given [name] in the Fluent Node DSL. */ fun LanguageFrontend<*, *>.t(name: CharSequence, generics: List = listOf()) = objectType(name, generics) diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/ThrowStatement.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/ThrowStatement.kt new file mode 100644 index 00000000000..27a8d1b200e --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/ThrowStatement.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.graph.statements + +import de.fraunhofer.aisec.cpg.graph.ArgumentHolder +import de.fraunhofer.aisec.cpg.graph.edges.ast.astOptionalEdgeOf +import de.fraunhofer.aisec.cpg.graph.edges.unwrapping +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression +import java.util.Objects +import org.apache.commons.lang3.builder.ToStringBuilder +import org.neo4j.ogm.annotation.Relationship + +/** Represents a `throw` or `raise` statement. */ +class ThrowStatement : Statement(), ArgumentHolder { + + /** The exception object to be raised. */ + @Relationship(value = "EXCEPTION") var exceptionEdge = astOptionalEdgeOf() + var exception by unwrapping(ThrowStatement::exceptionEdge) + + /** + * Some languages (Python) can add a parent exception (or `cause`) to indicate that an exception + * was raised while handling another exception. + */ + @Relationship(value = "PARENT_EXCEPTION") + var parentExceptionEdge = astOptionalEdgeOf() + var parentException by unwrapping(ThrowStatement::parentExceptionEdge) + + override fun addArgument(expression: Expression) { + when { + exception == null -> exception = expression + parentException == null -> parentException = expression + } + } + + override fun replaceArgument(old: Expression, new: Expression): Boolean { + return when { + exception == old -> { + exception = new + true + } + parentException == old -> { + parentException = new + true + } + else -> false + } + } + + override fun hasArgument(expression: Expression): Boolean { + return exception == expression || parentException == expression + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ThrowStatement) return false + return super.equals(other) && + exception == other.exception && + parentException == other.parentException + } + + override fun hashCode() = Objects.hash(super.hashCode(), exception, parentException) + + override fun toString(): String { + return ToStringBuilder(this, TO_STRING_STYLE) + .appendSuper(super.toString()) + .append("exception", exception) + .append("parentException", parentException) + .toString() + } +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/DFGPass.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/DFGPass.kt index 6d731705ac6..da018184f71 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/DFGPass.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/DFGPass.kt @@ -130,6 +130,7 @@ class DFGPass(ctx: TranslationContext) : ComponentPass(ctx) { is ForStatement -> handleForStatement(node) is SwitchStatement -> handleSwitchStatement(node) is IfStatement -> handleIfStatement(node) + is ThrowStatement -> handleThrowStatement(node) // Declarations is FieldDeclaration -> handleFieldDeclaration(node) is FunctionDeclaration -> handleFunctionDeclaration(node, functionSummaries) @@ -138,6 +139,12 @@ class DFGPass(ctx: TranslationContext) : ComponentPass(ctx) { } } + /** Handle a [ThrowStatement]. The exception and parent exception flow into the node. */ + protected fun handleThrowStatement(node: ThrowStatement) { + node.exception?.let { node.prevDFGEdges += it } + node.parentException?.let { node.prevDFGEdges += it } + } + protected fun handleAssignExpression(node: AssignExpression) { // If this is a compound assign, we also need to model a dataflow to the node itself if (node.isCompoundAssignment) { diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/EvaluationOrderGraphPass.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/EvaluationOrderGraphPass.kt index 8870b6a9b68..b215bb0be6e 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/EvaluationOrderGraphPass.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/EvaluationOrderGraphPass.kt @@ -160,6 +160,7 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa map[LookupScopeStatement::class.java] = { handleLookupScopeStatement(it as LookupScopeStatement) } + map[ThrowStatement::class.java] = { handleThrowStatement(it as ThrowStatement) } } protected fun doNothing() { @@ -546,27 +547,38 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa // TODO(oxisto): These operator codes are highly language specific and might be more suited // to be handled differently (see https://github.com/Fraunhofer-AISEC/cpg/issues/1161) if (node.operatorCode == "throw") { - handleThrowOperator(node) + handleThrowOperator(node, node.input) } else { handleUnspecificUnaryOperator(node) } } - protected fun handleThrowOperator(node: UnaryOperator) { - val input = node.input - createEOG(input) - - val catchingScope = - scopeManager.firstScopeOrNull { scope -> scope is TryScope || scope is FunctionScope } - - val throwType = input.type + /** + * Generates the EOG for a [node] which represents a statement/expression which throws an + * exception. Since some languages may accept different inputs to a throw statement (typically + * 1, sometimes 2, 0 is also possible), we have collect these in [inputs]. The input which is + * evaluated first, must be the first item in the vararg! Any `null` object in `inputs` will be + * filtered. We connect the throw statement internally, i.e., the inputs are evaluated from + * index 0 to n and then the whole node is evaluated. + */ + protected fun handleThrowOperator(node: Node, vararg inputs: Expression?) { + inputs.filterNotNull().forEach { createEOG(it) } pushToEOG(node) - if (catchingScope is TryScope) { - catchingScope.catchesOrRelays[throwType] = currentPredecessors.toMutableList() - } else if (catchingScope is FunctionScope) { - catchingScope.catchesOrRelays[throwType] = currentPredecessors.toMutableList() + + val input = inputs.firstOrNull() + val throwType = input?.type + if (throwType != null) { + val catchingScope = + scopeManager.firstScopeOrNull { scope -> + scope is TryScope || scope is FunctionScope + } + if (catchingScope is TryScope) { + catchingScope.catchesOrRelays[throwType] = currentPredecessors.toMutableList() + } else if (catchingScope is FunctionScope) { + catchingScope.catchesOrRelays[throwType] = currentPredecessors.toMutableList() + } } - currentPredecessors.clear() + currentPredecessors.clear() // TODO: Should this be here or inside the if statement? } /** @@ -1032,6 +1044,11 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa pushToEOG(stmt) } + /** This is copied & pasted with minimal adjustments from [handleThrowOperator]. */ + protected fun handleThrowStatement(statement: ThrowStatement) { + handleThrowOperator(statement, statement.exception, statement.parentException) + } + companion object { protected val LOGGER = LoggerFactory.getLogger(EvaluationOrderGraphPass::class.java) diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/GraphExamples.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/GraphExamples.kt index e4e55d1d924..db0679a64d8 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/GraphExamples.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/GraphExamples.kt @@ -1342,5 +1342,25 @@ class GraphExamples { } } } + + fun prepareThrowDFGTest( + config: TranslationConfiguration = + TranslationConfiguration.builder() + .defaultPasses() + .registerLanguage(TestLanguage(".")) + .build() + ) = + testFrontend(config).build { + translationResult { + translationUnit("some.file") { + function("foo", t("void")) { + body { + declare { variable("a", t("short")) { literal(42) } } + `throw` { call("SomeError") { ref("a") } } + } + } + } + } + } } } diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/ThrowStatementTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/ThrowStatementTest.kt new file mode 100644 index 00000000000..bb459a2984b --- /dev/null +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/ThrowStatementTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.graph + +import de.fraunhofer.aisec.cpg.GraphExamples.Companion.testFrontend +import de.fraunhofer.aisec.cpg.TranslationConfiguration +import de.fraunhofer.aisec.cpg.frontends.TestLanguage +import de.fraunhofer.aisec.cpg.graph.builder.* +import de.fraunhofer.aisec.cpg.graph.statements.ThrowStatement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.test.assertLocalName +import kotlin.test.* + +class ThrowStatementTest { + @Test + fun testThrow() { + val result = + testFrontend( + TranslationConfiguration.builder() + .defaultPasses() + .registerLanguage(TestLanguage(".")) + .build() + ) + .build { + translationResult { + translationUnit("some.file") { + function("foo", t("void")) { + body { + `throw` {} + `throw` { call("SomeError") } + `throw` { + call("SomeError") + call("SomeError2") + } + } + } + } + } + } + + // Let's assert that we did this correctly + val main = result.functions["foo"] + assertNotNull(main) + val body = main.body + assertIs(body) + + val emptyThrow = body.statements.getOrNull(0) + assertIs(emptyThrow) + println(emptyThrow.toString()) // This is only here to simulate a higher test coverage + assertNull(emptyThrow.exception) + assertTrue(emptyThrow.prevDFG.isEmpty()) + + val throwWithExc = body.statements.getOrNull(1) + assertIs(throwWithExc) + println(throwWithExc.toString()) // This is only here to simulate a higher test coverage + val throwCall = throwWithExc.exception + assertIs(throwCall) + assertLocalName("SomeError", throwCall) + assertEquals(setOf(throwCall), throwWithExc.prevDFG.toSet()) + + val throwWithExcAndParent = body.statements.getOrNull(2) + assertIs(throwWithExcAndParent) + println( + throwWithExcAndParent.toString() + ) // This is only here to simulate a higher test coverage + val throwCallException = throwWithExcAndParent.exception + assertIs(throwCallException) + assertLocalName("SomeError", throwCallException) + val throwCallParent = throwWithExcAndParent.parentException + assertIs(throwCallParent) + assertLocalName("SomeError2", throwCallParent) + assertEquals( + setOf(throwCallException, throwCallParent), + throwWithExcAndParent.prevDFG.toSet() + ) + } +} diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/edges/flows/DataflowTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/edges/flows/DataflowTest.kt index d01d9b9b60f..06b4c8ba708 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/edges/flows/DataflowTest.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/edges/flows/DataflowTest.kt @@ -25,13 +25,18 @@ */ package de.fraunhofer.aisec.cpg.graph.edges.flows +import de.fraunhofer.aisec.cpg.GraphExamples.Companion.prepareThrowDFGTest import de.fraunhofer.aisec.cpg.frontends.TestLanguageFrontend -import de.fraunhofer.aisec.cpg.graph.newLiteral -import de.fraunhofer.aisec.cpg.graph.newReference +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.statements.ThrowStatement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression import kotlin.collections.firstOrNull import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull import kotlin.test.assertSame class DataflowTest { @@ -101,4 +106,25 @@ class DataflowTest { assertEquals(0, node2.prevDFGEdges.size) } } + + @Test + fun testThrow() { + val result = prepareThrowDFGTest() + + // Let's assert that we did this correctly + val main = result.functions["foo"] + assertNotNull(main) + val body = main.body + assertIs(body) + + val throwStmt = body.statements.getOrNull(1) + assertIs(throwStmt) + assertNotNull(throwStmt.exception) + val throwCall = throwStmt.exception + assertIs(throwCall) + + val someError = result.calls["SomeError"] + assertIs(someError) + assertContains(throwStmt.prevDFG, someError) + } } diff --git a/docs/docs/CPG/specs/dfg.md b/docs/docs/CPG/specs/dfg.md index 8f004005ceb..cf0f786e04a 100755 --- a/docs/docs/CPG/specs/dfg.md +++ b/docs/docs/CPG/specs/dfg.md @@ -391,6 +391,23 @@ The data flow from the input to this node and, in case of the operatorCodes ++ a *Dangerous: We have to ensure that the first operation is performed before the last one (if applicable)* +## ThrowStatement + +Interesting fields: + +* `exception: Expression`: The exception which is thrown +* `parentException: Expression`: The exception which has originally caused this exception to be thrown (e.g. in a catch clause) + +The return value flows to the whole statement. + +Scheme: +```mermaid + flowchart LR + exception -- DFG --> node([ReturnStatement]); + parentException -- DFG --> node; + exception -.- node; + parentException -.- node; +``` ## ReturnStatement diff --git a/docs/docs/CPG/specs/eog.md b/docs/docs/CPG/specs/eog.md index b20f26daee8..12ec6b46e71 100644 --- a/docs/docs/CPG/specs/eog.md +++ b/docs/docs/CPG/specs/eog.md @@ -379,6 +379,27 @@ flowchart LR ``` +## ThrowStatement +The EOG continues at an exception catching structure or a function that does a re-throw. + +Interesting fields: + +* `exception: Expression`: Exception to be thrown for exception handling. +* `parentException: Expression`: Exception which caused this exception to be thrown. + +Scheme: +```mermaid +flowchart LR + classDef outer fill:#fff,stroke:#ddd,stroke-dasharray:5 5; + prev:::outer --EOG--> child1["exception"] + child1 --EOG--> child2["parentException"] + child2 --EOG-->parent + parent(["throw"]) --EOG--> catchingContext:::outer + parent -.-> child1 + parent -.-> child2 + +``` + ## AssertStatement Statement that evaluates a condition and if the condition is false, evaluates a message, this message is generalized to a `Statement` to hold everything