diff --git a/cpg-core/build.gradle.kts b/cpg-core/build.gradle.kts index 1fd52ed78d2..6347059bc21 100644 --- a/cpg-core/build.gradle.kts +++ b/cpg-core/build.gradle.kts @@ -54,7 +54,8 @@ dependencies { implementation(libs.kotlinx.coroutines.core) testImplementation(libs.junit.params) - integrationTestImplementation(libs.kotlin.reflect) + findProject(":cpg-language-java")?.also { integrationTestImplementation(it) } + findProject(":cpg-language-cxx")?.also { integrationTestImplementation(it) } testFixturesApi( libs.kotlin.test.junit5 diff --git a/cpg-core/src/integrationTest/kotlin/de/fraunhofer/aisec/cpg/IntegrationTest.kt b/cpg-core/src/integrationTest/kotlin/de/fraunhofer/aisec/cpg/IntegrationTest.kt index 6c347cc4cb7..022c975e1fa 100644 --- a/cpg-core/src/integrationTest/kotlin/de/fraunhofer/aisec/cpg/IntegrationTest.kt +++ b/cpg-core/src/integrationTest/kotlin/de/fraunhofer/aisec/cpg/IntegrationTest.kt @@ -25,15 +25,24 @@ */ package de.fraunhofer.aisec.cpg +import de.fraunhofer.aisec.cpg.frontends.ForeignFunctionInterface +import de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage +import de.fraunhofer.aisec.cpg.frontends.java.JavaLanguage +import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.declarations.Function import de.fraunhofer.aisec.cpg.graph.expressions.Call import de.fraunhofer.aisec.cpg.graph.functions +import de.fraunhofer.aisec.cpg.graph.scopes.Symbol +import de.fraunhofer.aisec.cpg.graph.types.Type import de.fraunhofer.aisec.cpg.persistence.createJsonGraph import de.fraunhofer.aisec.cpg.persistence.persistJson import de.fraunhofer.aisec.cpg.test.GraphExamples +import de.fraunhofer.aisec.cpg.test.analyze +import kotlin.io.path.Path import kotlin.io.path.createTempFile import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertTrue import org.junit.jupiter.api.Test /** @@ -42,6 +51,60 @@ import org.junit.jupiter.api.Test */ class IntegrationTest { + class JavaCFFI : + ForeignFunctionInterface(JavaLanguage(), CLanguage()) { + override fun mapSymbol(from: Symbol): Symbol { + val isNativeAdd = + from == "nativeAdd" || + from.endsWith(".nativeAdd") || + from.contains(".nativeAdd(") || + from.contains("nativeAdd(") + + return if (isNativeAdd) { + "Java_MyClass_nativeAdd" + } else { + from + } + } + + override fun mapType(from: Type): Type { + return to.builtInTypes[from.name.toString()] ?: from + } + } + + // + // --report-output="study/python_analysis_output.py" --path-suffix="-python" + + @Test + fun testForeignFunctionInterface() { + val topLevel = Path("src/integrationTest/resources/foreignFunctionInterface") + val result = + analyze( + listOf( + topLevel.resolve("MyClass.java").toFile(), + topLevel.resolve("native.c").toFile(), + ), + topLevel, + usePasses = true, + ) { + it.registerLanguage() + it.registerLanguage() + it.registerLanguageInterface() + } + assertNotNull(result) + + val useNativeAdd = result.functions["useNativeAdd"] + assertNotNull(useNativeAdd) + + val nativeCall = useNativeAdd.calls["nativeAdd"] + assertNotNull(nativeCall) + + val nativeImplementation = result.functions["Java_MyClass_nativeAdd"] + assertNotNull(nativeImplementation) + + assertTrue(nativeCall.invokes.contains(nativeImplementation)) + } + @Test fun testBuildJsonGraph() { val translationResult = GraphExamples.getInitializerListExprDFG() diff --git a/cpg-core/src/integrationTest/resources/foreignFunctionInterface/MyClass.java b/cpg-core/src/integrationTest/resources/foreignFunctionInterface/MyClass.java new file mode 100644 index 00000000000..f115a31a848 --- /dev/null +++ b/cpg-core/src/integrationTest/resources/foreignFunctionInterface/MyClass.java @@ -0,0 +1,14 @@ +public class MyClass { + // Declared in native.c via JNI naming convention. + public static native int nativeAdd(int left, int right); + + public static int useNativeAdd() { + return nativeAdd(4, 7); + } + + public static void main(String[] args) { + int result = useNativeAdd(); + System.out.println("JNI result: " + result); + } +} + diff --git a/cpg-core/src/integrationTest/resources/foreignFunctionInterface/native.c b/cpg-core/src/integrationTest/resources/foreignFunctionInterface/native.c new file mode 100644 index 00000000000..b2df2b6a956 --- /dev/null +++ b/cpg-core/src/integrationTest/resources/foreignFunctionInterface/native.c @@ -0,0 +1,8 @@ +#include + +JNIEXPORT jint JNICALL Java_MyClass_nativeAdd(JNIEnv *env, jclass cls, jint left, jint right) { + (void)env; + (void)cls; + return left + right; +} + diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt index c3b82d6ea64..605e49abf25 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt @@ -787,31 +787,32 @@ class ScopeManager(override var ctx: TranslationContext) : ScopeProvider, Contex // We need to differentiate between a qualified and unqualified lookup. We have a qualified // lookup, if the scope is not null. In this case we need to stay within the specified scope val list = - when { - scope != null -> { - scope - .lookupSymbol( - n.localName, - languageOnly = language, - qualifiedLookup = true, - replaceImports = replaceImports, - predicate = predicate, - ) - .toMutableList() - } - else -> { - // Otherwise, we can look up the symbol alone (without any FQN) starting from - // the startScope - startScope - ?.lookupSymbol( - n.localName, - languageOnly = language, - replaceImports = replaceImports, - predicate = predicate, - ) - ?.toMutableList() ?: mutableListOf() - } - } + findDeclarationsByName( + scope, + n.localName, + language, + replaceImports, + predicate, + startScope, + ) + + val otherLanguageInterfaces = ctx.languageInterfaces[language] + otherLanguageInterfaces?.forEach { otherLanguageInterface -> + val otherLanguage = otherLanguageInterface.to + val symbol = otherLanguageInterface.mapSymbol(n.localName) + + val toAdd = + findDeclarationsByName( + scope, + symbol, + otherLanguage, + replaceImports, + predicate, + startScope, + ) + + list.addAll(toAdd) + } // If we have both the definition and the declaration of a function declaration in our list, // we chose only the definition @@ -829,6 +830,41 @@ class ScopeManager(override var ctx: TranslationContext) : ScopeProvider, Contex return list } + private fun findDeclarationsByName( + scope: Scope?, + symbol: Symbol, + language: Language<*>, + replaceImports: Boolean, + predicate: ((Declaration) -> Boolean)?, + startScope: Scope?, + ): MutableList = + when { + scope != null -> { + scope + .lookupSymbol( + symbol, + languageOnly = language, + qualifiedLookup = true, + replaceImports = replaceImports, + predicate = predicate, + ) + .toMutableList() + } + + else -> { + // Otherwise, we can look up the symbol alone (without any FQN) starting from + // the startScope + startScope + ?.lookupSymbol( + symbol, + languageOnly = language, + replaceImports = replaceImports, + predicate = predicate, + ) + ?.toMutableList() ?: mutableListOf() + } + }.toMutableList() + /** * This function tries to look up the symbol contained in [name] (using [lookupSymbolByName]) * and returns a [DeclaresType] node, if this name resolved to something which declares a type. diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt index 4d36545b994..4e5c75f2ea9 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt @@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize import de.fraunhofer.aisec.cpg.TranslationContext.EmptyTranslationContext import de.fraunhofer.aisec.cpg.TranslationResult.Companion.DEFAULT_APPLICATION_NAME import de.fraunhofer.aisec.cpg.frontends.CompilationDatabase +import de.fraunhofer.aisec.cpg.frontends.ForeignFunctionInterface import de.fraunhofer.aisec.cpg.frontends.FrontendConfiguration import de.fraunhofer.aisec.cpg.frontends.KClassSerializer import de.fraunhofer.aisec.cpg.frontends.Language @@ -114,6 +115,7 @@ private constructor( /** This list contains the files with function summaries which should be considered. */ val functionSummaries: DFGFunctionSummaries, languages: Set>>, + languageInterfaces: Set, out Language<*>>>>, codeInNodes: Boolean, processAnnotations: Boolean, disableCleanup: Boolean, @@ -138,6 +140,11 @@ private constructor( /** This list contains all languages which we want to translate. */ @JsonIgnore val languages: Set>> + /** This list contains all language interfaces. */ + @JsonIgnore + val languageInterfaces: + Set, out Language<*>>>> + /** * Switch off cleaning up TypeManager memory after analysis. * @@ -207,6 +214,7 @@ private constructor( init { this.registeredPasses = passes this.languages = languages + this.languageInterfaces = languageInterfaces // Make sure to init this AFTER sourceLocations has been set this.codeInNodes = codeInNodes this.processAnnotations = processAnnotations @@ -248,6 +256,8 @@ private constructor( class Builder { private var softwareComponents: MutableMap> = HashMap() private val languages = mutableSetOf>>() + private val languageInterfaces = + mutableSetOf, out Language<*>>>>() private var topLevels = mutableMapOf() private var debugParser = false private var failOnError = false @@ -477,6 +487,22 @@ private constructor( return this } + /** Registers an additional [ForeignFunctionInterface] by its [KClass]. */ + fun < + T : ForeignFunctionInterface, out Language<*>> + > registerLanguageInterface(clazz: KClass): Builder { + languageInterfaces.add(clazz) + return this + } + + /** Registers an additional [ForeignFunctionInterface] */ + inline fun < + reified T : ForeignFunctionInterface, out Language<*>> + > registerLanguageInterface(): Builder { + registerLanguageInterface(T::class) + return this + } + /** * Configures a [LanguageFrontend] with a [FrontendConfiguration] [config]. This allows us * to pass additional information to the frontend, such methods which should not be analyzed @@ -742,6 +768,7 @@ private constructor( replacedPasses, DFGFunctionSummaries.fromFiles(functionSummaries), languages, + languageInterfaces, codeInNodes, processAnnotations, disableCleanup, diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt index 3fb425d5c38..82dab813b14 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt @@ -27,6 +27,7 @@ package de.fraunhofer.aisec.cpg import de.fraunhofer.aisec.cpg.TranslationManager.AdditionalSource import de.fraunhofer.aisec.cpg.TranslationManager.Companion.log +import de.fraunhofer.aisec.cpg.frontends.ForeignFunctionInterface import de.fraunhofer.aisec.cpg.frontends.Language import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend import de.fraunhofer.aisec.cpg.graph.Component @@ -111,6 +112,20 @@ open class TranslationContext( } } + val languageInterfaces: + Map< + Language<*>?, + Collection, out Language<*>>>, + > by lazy { + val list = + config.languageInterfaces.map { + val li = it.constructors.firstOrNull()?.call() + li?.from to li + } + val m = list.groupBy({ it.first }, { it.second }) + m.map { (k, v) -> k to v.filterNotNull() }.toMap() + } + /** * This extension function returns an appropriate [Language] for this [File] based on the * [availableLanguages], which in turn are based on the registered file extensions of diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ForeignFunctionInterface.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ForeignFunctionInterface.kt new file mode 100644 index 00000000000..1196b9a4f99 --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ForeignFunctionInterface.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026, 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 + +import de.fraunhofer.aisec.cpg.graph.scopes.Symbol +import de.fraunhofer.aisec.cpg.graph.types.Type + +/** + * The CPG can hold nodes of different programming languages at the same time. There are several + * frameworks which allow calling functions from one language to another one. Examples which we also + * use in this repository are JNI or jep. However, the + * [de.fraunhofer.aisec.cpg.passes.SymbolResolver] won't be able to resolve functions across such + * interfaces for several reasons: First, it expects that the symbol and the declaration it refers + * to are of the same language. Second, the interfaces may slightly change the name and for sure the + * [Type]s (since each type has its own language). + * + * This class provides a way to specify such interfaces between two programming languages. It models + * the way to resolve a symbol from the language [from] (e.g., caller) to the language [to] (e.g., + * callee, declaration). + */ +abstract class ForeignFunctionInterface, TO : Language<*>>( + val from: FROM, + val to: TO, +) { + + /** + * Maps the [Symbol] [from] the [FROM] language to a [Symbol] of the [TO] language. This is + * necessary to ensure that we can properly connect the graph nodes of the [FROM] language to + * the graph nodes of the [TO] language. + */ + abstract fun mapSymbol(from: Symbol): Symbol + + /** + * Maps the [Type] [from] the [FROM] language to the [Type] of the [TO] language. This is + * necessary to ensure that we can properly connect the graph nodes of the [FROM] language to + * the graph nodes of the [TO] language. + */ + abstract fun mapType(from: Type): Type +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt index eb74ca4001d..7711fc48ec4 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt @@ -153,7 +153,7 @@ sealed class Scope( * can be used for additional filtering. * * By default, the lookup algorithm will go to the [Scope.parent] if no match was found in the - * current scope. This behaviour can be turned off with [qualifiedLookup]. This is useful for + * current scope. This behavior can be turned off with [qualifiedLookup]. This is useful for * qualified lookups, where we want to stay in our lookup-scope. * * We need to consider the language trait [HasImplicitReceiver] here as well. If the language diff --git a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt new file mode 100644 index 00000000000..ed51b666a02 --- /dev/null +++ b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, 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.frontends.ForeignFunctionInterface +import de.fraunhofer.aisec.cpg.graph.scopes.Symbol +import de.fraunhofer.aisec.cpg.graph.types.Type + +class CToCxxMapper : ForeignFunctionInterface(CLanguage(), CPPLanguage()) { + override fun mapSymbol(from: Symbol): Symbol { + return from + } + + override fun mapType(from: Type): Type { + return from + } +} + +class CxxToCMapper : ForeignFunctionInterface(CPPLanguage(), CLanguage()) { + override fun mapSymbol(from: Symbol): Symbol { + return from + } + + override fun mapType(from: Type): Type { + return from + } +} diff --git a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt new file mode 100644 index 00000000000..cd50521ea06 --- /dev/null +++ b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026, 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.enhancements + +import de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage +import de.fraunhofer.aisec.cpg.frontends.cxx.CPPLanguage +import de.fraunhofer.aisec.cpg.frontends.cxx.CToCxxMapper +import de.fraunhofer.aisec.cpg.frontends.cxx.CxxToCMapper +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.test.analyze +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class LanguageInterfaceTest { + private val topLevel = Path.of("src", "test", "resources", "crossLanguage") + + @Test + @Throws(Exception::class) + fun testCtoCPP() { + val result = + analyze( + files = + listOf( + topLevel.resolve("simple.c").toFile(), + topLevel.resolve("simpleCxx.cpp").toFile(), + ), + topLevel = topLevel, + usePasses = true, + ) { + it.registerLanguage() + it.registerLanguage() + it.registerLanguageInterface() + it.registerLanguageInterface() + } + + val main = result.functions["main"] + assertNotNull(main) + val call = main.calls["hello_world"] + assertNotNull(call) + + val helloWorlds = result.functions("hello_world") + assertEquals(1, helloWorlds.size) + val helloWorld = helloWorlds.single() + + assertEquals(helloWorld, call.invokes.singleOrNull()) + } +} diff --git a/cpg-language-cxx/src/test/resources/crossLanguage/simple.c b/cpg-language-cxx/src/test/resources/crossLanguage/simple.c new file mode 100644 index 00000000000..8727b4ae131 --- /dev/null +++ b/cpg-language-cxx/src/test/resources/crossLanguage/simple.c @@ -0,0 +1,5 @@ +#include + +void hello_world(void) { + printf("Hello from C!\n"); +} \ No newline at end of file diff --git a/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp b/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp new file mode 100644 index 00000000000..501e815cfb4 --- /dev/null +++ b/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp @@ -0,0 +1,4 @@ +int main() { + hello_world(); + return 0; +} \ No newline at end of file