From cd60b5e6c5c3a26982e07239a83247b07472ca48 Mon Sep 17 00:00:00 2001 From: Mathias Morbitzer Date: Thu, 18 Jun 2026 16:18:27 +0200 Subject: [PATCH 1/3] new test --- .../de/fraunhofer/aisec/cpg/FollowXTest.kt | 62 +++++++++++++++++++ .../src/test/resources/complex_dfg.c | 29 +++++++++ 2 files changed, 91 insertions(+) create mode 100644 cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/FollowXTest.kt create mode 100644 cpg-language-cxx/src/test/resources/complex_dfg.c diff --git a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/FollowXTest.kt b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/FollowXTest.kt new file mode 100644 index 00000000000..de208f9a2fe --- /dev/null +++ b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/FollowXTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025, 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.frontends.cxx + +import de.fraunhofer.aisec.cpg.graph.expressions.Call +import de.fraunhofer.aisec.cpg.graph.followDFGEdgesUntilHit +import de.fraunhofer.aisec.cpg.graph.functions +import de.fraunhofer.aisec.cpg.graph.invoke +import de.fraunhofer.aisec.cpg.graph.variables +import de.fraunhofer.aisec.cpg.test.BaseTest +import de.fraunhofer.aisec.cpg.test.analyze +import java.io.File +import kotlin.test.Test +import org.junit.jupiter.api.assertNotNull + +class FollowXTest : BaseTest() { + @Test + fun testFollowX() { + val file = File("src/test/resources/complex_dfg.c") + val result = + analyze(listOf(file), file.parentFile.toPath(), true) { + it.registerLanguage() + } + assertNotNull(result) + + // functions + val mainFunc = result.functions("main").single() + + // Variables + val i = mainFunc.variables("i").single() + + // actual tests + val paths = + i.followDFGEdgesUntilHit( + predicate = { node -> (node as? Call)?.name?.localName == "printf" } + ) + println(paths) + } +} diff --git a/cpg-language-cxx/src/test/resources/complex_dfg.c b/cpg-language-cxx/src/test/resources/complex_dfg.c new file mode 100644 index 00000000000..f5881ff4829 --- /dev/null +++ b/cpg-language-cxx/src/test/resources/complex_dfg.c @@ -0,0 +1,29 @@ +#include + +int func1(); + +int func3(int *p){ + printf("%d\n", *p); + func1(p); +} + +int func2(int* p) { + for (int j=0; j<10; j++) { + (*p)++; + } + func3(p); +} + +int func1(int* p) { + if (*p < 50) { + (*p)++; + func2(p); + } +} + +int main() { + int i=0; + int* p=&i; + func1(p); + return 0; +} From 4f195e8460e91fa20e2fce7f0c633589c63e0fc5 Mon Sep 17 00:00:00 2001 From: Mathias Morbitzer Date: Fri, 19 Jun 2026 09:30:31 +0200 Subject: [PATCH 2/3] fix pmv dfgs of recursive functions --- .../aisec/cpg/passes/PointsToPass.kt | 78 ++++++++++--------- .../aisec/cpg/passes/PointsToPassTest.kt | 23 ++++++ .../src/test/resources/complex_dfg.c | 17 ++-- 3 files changed, 75 insertions(+), 43 deletions(-) diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/PointsToPass.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/PointsToPass.kt index 195b32b2623..369d0196533 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/PointsToPass.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/PointsToPass.kt @@ -587,18 +587,22 @@ open class PointsToPass(ctx: TranslationContext) : EOGStarterPass(ctx, orderDepe // Then the memoryValues if (key is HasMemoryValue && value.second.isNotEmpty()) { value.second.forEach { (v, properties) -> - var granularity = default() - var shortFS = false - properties.forEach { p -> - when (p) { - // String indicated a partial target - is String -> granularity = PartialDataflowGranularity(p) - // Boolean says if this is a shortFS or not - is Boolean -> shortFS = p - else -> TODO() + // We already added the parameters value in initializeParameters, so we skip + // that now + if (v !is Parameter) { + var granularity = default() + var shortFS = false + properties.forEach { p -> + when (p) { + // String indicated a partial target + is String -> granularity = PartialDataflowGranularity(p) + // Boolean says if this is a shortFS or not + is Boolean -> shortFS = p + else -> TODO() + } } + key.memoryValueEdges += Dataflow(v, key, granularity, shortFS) } - key.memoryValueEdges += Dataflow(v, key, granularity, shortFS) } } @@ -935,15 +939,9 @@ open class PointsToPass(ctx: TranslationContext) : EOGStarterPass(ctx, orderDepe mutableSetOf( NodeWithPropertiesKey( matchingDeclarations, - // Add the parameter - // index to indicate - // to the - // calculatePrevDFGs - // function that we - // need to replace - // the - // value of the call - // argument + // Add the parameter index to indicate to the + // calculatePrevDFGs function that we need to + // replace the value of the call argument equalLinkedHashSetOf( matchingDeclarations.argumentIndex ), @@ -1920,6 +1918,20 @@ open class PointsToPass(ctx: TranslationContext) : EOGStarterPass(ctx, orderDepe } private suspend fun calculateFunctionSummaries(invoke: Function): Function? { + fun addDummyFS() { + if (log.isTraceEnabled) { + log.trace("Creating dummy function summary") + } + val newValues = ConcurrentHashMap.newKeySet() + invoke.parameters.forEach { newValues.add(FSEntry(0, it, 1, "", isDummy = true)) } + val entries = identitySetOf() + if (invoke.returns.isNotEmpty()) entries.addAll(invoke.returns) else entries.add(invoke) + entries.forEach { entry -> invoke.functionSummary.put(entry, newValues) } + if (log.isTraceEnabled) { + log.trace("Finished creating dummy function summary") + } + } + if (invoke.functionSummary.isEmpty()) { if (invoke.hasBody()) { if (log.isTraceEnabled) { @@ -1970,28 +1982,13 @@ open class PointsToPass(ctx: TranslationContext) : EOGStarterPass(ctx, orderDepe log.error( "Cannot calculate functionSummary for ${invoke.name.localName} as it's recursively called. callChain: ${functionSummaryAnalysisChain.map{it.name.localName}}" ) - invoke.functionSummary.put( - newLiteral("dummy"), - ConcurrentHashMap.newKeySet(), - ) - return null + addDummyFS() } } else { // Add a dummy function summary so that we don't try this every time // In this dummy, all parameters point either to returns if we have any, or the // Function itself - if (log.isTraceEnabled) { - log.trace("Creating dummy function summary") - } - val newValues = ConcurrentHashMap.newKeySet() - invoke.parameters.forEach { newValues.add(FSEntry(0, it, 1, "")) } - val entries = identitySetOf() - if (invoke.returns.isNotEmpty()) entries.addAll(invoke.returns) - else entries.add(invoke) - entries.forEach { entry -> invoke.functionSummary.put(entry, newValues) } - if (log.isTraceEnabled) { - log.trace("Finished creating dummy function summary") - } + addDummyFS() } } return invoke @@ -3208,6 +3205,11 @@ open class PointsToPass(ctx: TranslationContext) : EOGStarterPass(ctx, orderDepe PowersetLattice.Element(), ), ) + // Additionally, directly add the value to the node. This is necessary on recursive + // calls, when we are not yet finished with acceptInternal of one function, but we + // already need to fetch the PMVs because we have a recursive call while handling + // the function + src.memoryValueEdges += Dataflow(pmv, src) } else { // Link the PMVs with each other so that we can find them. This is // especially important outside the respective function where we @@ -3249,6 +3251,10 @@ open class PointsToPass(ctx: TranslationContext) : EOGStarterPass(ctx, orderDepe PowersetLattice.Element(), ), ) + // Here again, directly add the value to the param to be able to access it directly + // instead of having to wait until acceptInternal is finished + param.memoryValueEdges += + Dataflow(pmv, param, granularity = PartialDataflowGranularity(pmvName)) } // Update the states diff --git a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/PointsToPassTest.kt b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/PointsToPassTest.kt index eb3a37cafba..bc5103fc4a6 100644 --- a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/PointsToPassTest.kt +++ b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/PointsToPassTest.kt @@ -36,6 +36,7 @@ import de.fraunhofer.aisec.cpg.graph.declarations.Variable import de.fraunhofer.aisec.cpg.graph.edges.flows.* import de.fraunhofer.aisec.cpg.graph.expressions.* import de.fraunhofer.aisec.cpg.helpers.toIdentitySet +import de.fraunhofer.aisec.cpg.test.analyze import de.fraunhofer.aisec.cpg.test.analyzeAndGetFirstTU import de.fraunhofer.aisec.cpg.test.assertLocalName import java.io.File @@ -5005,4 +5006,26 @@ class PointsToPassTest { ) } } + + @Test + fun testDerefPMVsOfRecursiveFunctions() { + val file = File("src/test/resources/complex_dfg.c") + val result = + analyze(listOf(file), file.parentFile.toPath(), true) { + it.registerLanguage() + } + assertNotNull(result) + + // Functions + val func2 = result.functions("func2").single { it.body != null } + val func3 = result.functions("func3").single { it.body != null } + + // Params and PMVs + val func2Param = func2.parameters.single() + val func2DerefPMV = func2Param.memoryValues.single { it.name.localName == "derefvalue" } + + // actual tests + assertEquals(2, func2DerefPMV.prevDFG.size) + assertContains(func2DerefPMV.prevFullDFG, func3.allChildren().single().input) + } } diff --git a/cpg-language-cxx/src/test/resources/complex_dfg.c b/cpg-language-cxx/src/test/resources/complex_dfg.c index f5881ff4829..88bf3ccc759 100644 --- a/cpg-language-cxx/src/test/resources/complex_dfg.c +++ b/cpg-language-cxx/src/test/resources/complex_dfg.c @@ -1,29 +1,32 @@ #include -int func1(); +int func1(int* p); +int func2(int* p); +int func3(int* p); int func3(int *p){ printf("%d\n", *p); - func1(p); + (*p)++; + func2(p); } int func2(int* p) { for (int j=0; j<10; j++) { (*p)++; } - func3(p); + if (*p < 50) + func3(p); } int func1(int* p) { - if (*p < 50) { - (*p)++; - func2(p); - } + (*p)++; + func2(p); } int main() { int i=0; int* p=&i; func1(p); + printf("%d\n", i); return 0; } From b0acad436d1527ed8021c1f4f8223c7667c6a396 Mon Sep 17 00:00:00 2001 From: Mathias Morbitzer Date: Fri, 19 Jun 2026 10:18:00 +0200 Subject: [PATCH 3/3] change test to provoke infinite loop --- .../de/fraunhofer/aisec/cpg/FollowXTest.kt | 20 +++++++++++-------- .../src/test/resources/complex_dfg.c | 4 +--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/FollowXTest.kt b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/FollowXTest.kt index de208f9a2fe..4809f962bb5 100644 --- a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/FollowXTest.kt +++ b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/FollowXTest.kt @@ -25,8 +25,6 @@ */ package de.fraunhofer.aisec.cpg.frontends.cxx -import de.fraunhofer.aisec.cpg.graph.expressions.Call -import de.fraunhofer.aisec.cpg.graph.followDFGEdgesUntilHit import de.fraunhofer.aisec.cpg.graph.functions import de.fraunhofer.aisec.cpg.graph.invoke import de.fraunhofer.aisec.cpg.graph.variables @@ -50,13 +48,19 @@ class FollowXTest : BaseTest() { val mainFunc = result.functions("main").single() // Variables - val i = mainFunc.variables("i").single() + val i = result.variables("i").single() // actual tests - val paths = - i.followDFGEdgesUntilHit( - predicate = { node -> (node as? Call)?.name?.localName == "printf" } - ) - println(paths) + // This will run forever (or very long), so it's commented out. + // TODO: Fix + /* val (paths, time) = + measureTimedValue { + i.followDFGEdgesUntilHit( + findAllPossiblePaths = true, + sensitivities = OnlyFullDFG + FieldSensitive + ContextSensitive, + predicate = { node -> (node as? Reference)?.name?.localName == "i" }, + ) + } + println("Path lookup took $time")*/ } } diff --git a/cpg-language-cxx/src/test/resources/complex_dfg.c b/cpg-language-cxx/src/test/resources/complex_dfg.c index 88bf3ccc759..45da7f0d040 100644 --- a/cpg-language-cxx/src/test/resources/complex_dfg.c +++ b/cpg-language-cxx/src/test/resources/complex_dfg.c @@ -12,10 +12,8 @@ int func3(int *p){ int func2(int* p) { for (int j=0; j<10; j++) { - (*p)++; - } - if (*p < 50) func3(p); + } } int func1(int* p) {