From 3ed34188c5706b608c1576d7034c5331a22c39d4 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 24 Jul 2026 17:23:18 +0200 Subject: [PATCH 1/4] fix(dataflow): don't inline invokedynamic bootstrap methods in alias analysis Alias analysis crashed with IllegalStateException("Incorrect argument idx: 4") while analyzing record-bearing code (e.g. Netflix Conductor's openai.api package). A record's auto-generated equals/hashCode/toString lowers to an invokedynamic bootstrapped by java.lang.runtime.ObjectMethods.bootstrap, which declares six parameters while the dynamic call site supplies one. resolveCallNoCache resolved that call to the six-parameter bootstrap and inlined it; NestedCallInstEvalCtx then mapped the callee's parameters onto the one-element call.args and indexed past the end, aborting the whole package unit's alias analysis (on Conductor this silently suppressed ~60% of findings). Guard at the resolution boundary: never inline a callee that declares more parameters than the call site provides, since its parameters cannot be soundly mapped onto the arguments. The call becomes opaque for alias analysis -- the conservative default for an unanalyzable callee -- and createArg's out-of-range check stays a genuine invariant assertion rather than a silent fallback that fabricates alias facts. Baked by test: AliasSampleTest.`record invokedynamic bootstrap does not overflow alias arg mapping` inlines Payload.hashCode() at depth 2 and asserts the analysis does not throw; it fails with "Incorrect argument idx" when the guard is removed. The alias samples module moves to Java 17 so record samples compile -- existing samples use no invokedynamic constructs and their behaviour is unchanged (full AliasSampleTest green). --- .../samples/build.gradle.kts | 9 +++- .../java/sample/alias/RecordAliasSample.java | 27 ++++++++++ .../jvm/ap/ifds/alias/InterProcCallNode.kt | 14 +++++ .../jvm/ap/ifds/alias/AliasSampleTest.kt | 51 +++++++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/RecordAliasSample.java diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts index c128a5c94..0f080962f 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts @@ -4,8 +4,13 @@ plugins { tasks { withType { - sourceCompatibility = JavaVersion.VERSION_1_8.toString() - targetCompatibility = JavaVersion.VERSION_1_8.toString() + // Java 17 (was 1.8) so record samples compile: records lower equals/ + // hashCode/toString to invokedynamic bootstrapped by + // java.lang.runtime.ObjectMethods, which RecordAliasSample exercises to + // pin the arity guard in resolveCallNoCache. Existing samples use no + // invokedynamic constructs, so their bytecode shape is unchanged. + sourceCompatibility = JavaVersion.VERSION_17.toString() + targetCompatibility = JavaVersion.VERSION_17.toString() options.compilerArgs.add("-g") } } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/RecordAliasSample.java b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/RecordAliasSample.java new file mode 100644 index 000000000..01294da9a --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/RecordAliasSample.java @@ -0,0 +1,27 @@ +package sample.alias; + +// Records lower equals/hashCode/toString to an invokedynamic bootstrapped by +// java.lang.runtime.ObjectMethods.bootstrap, which declares 6 parameters while +// the dynamic call site supplies one (the record instance). When alias analysis +// inlines the record method and then its bootstrap, mapping the 6 parameters +// onto the single call argument reads arguments the call never provided. This +// sample reproduces exactly that shape so the arity guard in resolveCallNoCache +// can be pinned: without the guard the analysis crashes with +// "Incorrect argument idx"; with it the bootstrap call is treated as opaque. +public class RecordAliasSample { + + public record Payload(Object value) {} + + static void recordHashCodeInlined(Object src) { + Payload p = new Payload(src); + // p.hashCode() is a record method whose body is the ObjectMethods + // invokedynamic; alias analysis inlines it (and the bootstrap) at depth 2. + p.hashCode(); + // echo is a local var so the test can query its aliases (which triggers + // the full alias computation, and with it the record-method inlining). + Object echo = p.value(); + sinkOneValue(echo); + } + + static void sinkOneValue(Object v) { } +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt index 35a625319..4d2c68bd7 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt @@ -78,6 +78,10 @@ private class ResolvedCall(val methods: Map?) { } private class NestedCallInstEvalCtx(val call: Stmt.Call, val ctx: ContextInfo) : InstEvalContext { + // A well-formed resolution never asks for an argument the call does not have: + // resolveCallNoCache only inlines callees whose arity matches call.args, so + // every JIRArgument index the callee's body references is in range. If this + // fails, the resolution invariant upstream was violated -- surface it. override fun createArg(idx: Int): Value = call.args.getOrNull(idx) ?: error("Incorrect argument idx: $idx") @@ -92,6 +96,16 @@ private fun resolveCallNoCache(stmt: Stmt.Call, ctx: ContextInfo, callResolver: ?: return ResolvedCall.empty val resolvedCall = methods.mapIndexedNotNull { idx, method -> + // Never inline a callee that declares more parameters than the call site + // provides: mapping its parameters onto call.args would read arguments the + // call never passed (createArg would index past call.args). This happens + // when a call site resolves to a method that is not its real callee -- an + // `invokedynamic` such as a record's auto-generated equals/hashCode/toString + // resolves to its bootstrap method java.lang.runtime.ObjectMethods.bootstrap, + // which declares 6 parameters while the dynamic call site supplies one. + // Treat such a call as opaque for alias analysis instead of crashing. + if (method.parameters.size > stmt.args.size) return@mapIndexedNotNull null + val graph = callResolver.buildMethodGraph(method) ?: return@mapIndexedNotNull null diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt index e713d55a4..37eafc7f5 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt @@ -1,6 +1,7 @@ package org.opentaint.dataflow.jvm.ap.ifds.alias import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertDoesNotThrow import org.junit.jupiter.api.TestInstance import org.opentaint.dataflow.ap.ifds.AccessPathBase import org.opentaint.dataflow.ap.ifds.AccessPathBase.Companion.Argument @@ -577,6 +578,25 @@ class AliasSampleTest : BasicTestUtils() { assertTrue { aa.sinkArgApAliases(sink).isNotEmpty() } } + @Test + fun `record invokedynamic bootstrap does not overflow alias arg mapping`() { + val method = findMethod(RECORD_SAMPLE, "recordHashCodeInlined") + + // depth 2: recordHashCodeInlined (level 0) -> Payload.hashCode() (level 1) + // -> ObjectMethods.bootstrap (level 2). The record method lowers to an + // invokedynamic whose bootstrap declares 6 parameters, while the dynamic + // call site supplies 1. Querying an alias runs the full computation, which + // inlines that chain. Without the arity guard in resolveCallNoCache the + // nested-call arg mapping indexes past call.args and throws + // IllegalStateException("Incorrect argument idx"); with it the bootstrap + // call is skipped as opaque and the analysis completes. + assertDoesNotThrow { + val aa = aaForMethodKnowingObjectMethods(method, depth = 2) + val sink = method.findSinkCall("sinkOneValue") + aa.sinkArgApAliases(sink) + } + } + private fun aaForMethod( method: JIRMethod, params: JIRLocalAliasAnalysis.Params = JIRLocalAliasAnalysis.Params() @@ -592,6 +612,29 @@ class AliasSampleTest : BasicTestUtils() { return JIRLocalAliasAnalysis(ep, graph, callResolver, noRules, localReachability, cancellation, manager, params) } + private fun aaForMethodKnowingObjectMethods(method: JIRMethod, depth: Int): JIRLocalAliasAnalysis { + val ep = method.instList.first() + val usages = runBlocking { cp.usagesExt() } + val graph = JApplicationGraphImpl(cp, usages) + + // Treat both the sample location and java.lang.runtime.ObjectMethods as + // known so the record method AND its invokedynamic bootstrap resolve to + // inlinable graphs -- reproducing the condition under which the crash + // occurred (a production model where the bootstrap method was resolvable). + val sampleLoc = method.enclosingClass.declaration.location + val objectMethodsLoc = + cp.findClassOrNull("java.lang.runtime.ObjectMethods")?.declaration?.location + val knownLocs = setOfNotNull(sampleLoc, objectMethodsLoc) + + val callResolver = JIRCallResolver(cp, MultiLocationUnit(knownLocs)) + val localReachability = JIRLocalVariableReachability(method, graph, manager) + val cancellation = Cancellation().also { it.activate() } + + return JIRLocalAliasAnalysis( + ep, graph, callResolver, noRules, localReachability, cancellation, manager, interProcParams(depth) + ) + } + private fun interProcParams(depth: Int) = JIRLocalAliasAnalysis.Params(useAliasAnalysis = true, aliasAnalysisInterProcCallDepth = depth) @@ -631,6 +674,13 @@ class AliasSampleTest : BasicTestUtils() { override fun locationIsUnknown(loc: RegisteredLocation): Boolean = loc != this.loc } + private class MultiLocationUnit(val locs: Set) : JIRUnitResolver { + override fun resolve(method: JIRMethod): UnitType = + if (method.enclosingClass.declaration.location in locs) SingletonUnit else UnknownUnit + + override fun locationIsUnknown(loc: RegisteredLocation): Boolean = loc !in locs + } + companion object { const val ALIAS_SAMPLE_PKG = "sample.alias" const val SIMPLE_SAMPLE = "$ALIAS_SAMPLE_PKG.SimpleAliasSample" @@ -639,6 +689,7 @@ class AliasSampleTest : BasicTestUtils() { const val INTERPROC_SAMPLE = "$ALIAS_SAMPLE_PKG.InterProcAliasSample" const val FLAKY_SAMPLE = "$ALIAS_SAMPLE_PKG.FlakyAliasSample" const val HEADER_VALUES_SAMPLE = "sample.alias.HeaderValuesHangSample" + const val RECORD_SAMPLE = "$ALIAS_SAMPLE_PKG.RecordAliasSample" private const val FIELD_VALUE = "value" private const val FIELD_BOX = "box" From 7458b88ff0c4ff9bbe8784f625d0a8c86ca54b46 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 24 Jul 2026 10:27:59 +0200 Subject: [PATCH 2/4] fix(dataflow): reject alias targets with incompatible arity --- .../jvm/ap/ifds/alias/InterProcCallNode.kt | 17 ++--- .../ap/ifds/alias/InterProcCallNodeTest.kt | 62 +++++++++++++++++++ 2 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNodeTest.kt diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt index 4d2c68bd7..9c16af16e 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt @@ -78,10 +78,6 @@ private class ResolvedCall(val methods: Map?) { } private class NestedCallInstEvalCtx(val call: Stmt.Call, val ctx: ContextInfo) : InstEvalContext { - // A well-formed resolution never asks for an argument the call does not have: - // resolveCallNoCache only inlines callees whose arity matches call.args, so - // every JIRArgument index the callee's body references is in range. If this - // fails, the resolution invariant upstream was violated -- surface it. override fun createArg(idx: Int): Value = call.args.getOrNull(idx) ?: error("Incorrect argument idx: $idx") @@ -96,15 +92,10 @@ private fun resolveCallNoCache(stmt: Stmt.Call, ctx: ContextInfo, callResolver: ?: return ResolvedCall.empty val resolvedCall = methods.mapIndexedNotNull { idx, method -> - // Never inline a callee that declares more parameters than the call site - // provides: mapping its parameters onto call.args would read arguments the - // call never passed (createArg would index past call.args). This happens - // when a call site resolves to a method that is not its real callee -- an - // `invokedynamic` such as a record's auto-generated equals/hashCode/toString - // resolves to its bootstrap method java.lang.runtime.ObjectMethods.bootstrap, - // which declares 6 parameters while the dynamic call site supplies one. - // Treat such a call as opaque for alias analysis instead of crashing. - if (method.parameters.size > stmt.args.size) return@mapIndexedNotNull null + // Override approximation may return a bridge or synthetic target with a different + // descriptor. Such a method cannot be the target of this call and must not enter the + // nested analysis with arguments mapped to the wrong positions. + if (method.parameters.size != stmt.args.size) return@mapIndexedNotNull null val graph = callResolver.buildMethodGraph(method) ?: return@mapIndexedNotNull null diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNodeTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNodeTest.kt new file mode 100644 index 000000000..6fd200864 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNodeTest.kt @@ -0,0 +1,62 @@ +package org.opentaint.dataflow.jvm.ap.ifds.alias + +import org.opentaint.dataflow.ap.ifds.analysis.alias.ContextInfo +import org.opentaint.dataflow.jvm.ap.ifds.alias.DSUAliasAnalysis.ResolvedCallMethod +import org.opentaint.dataflow.jvm.ap.ifds.alias.JIRIntraProcAliasAnalysis.JIRInstGraph +import org.opentaint.dataflow.jvm.ap.ifds.alias.RefValue.Local +import org.opentaint.ir.api.jvm.JIRMethod +import java.lang.reflect.Proxy +import kotlin.test.Test +import kotlin.test.assertNull + +class InterProcCallNodeTest { + @Test + fun `call resolution rejects a target with incompatible arity`() { + val callMethod = method(parameterCount = 0) + val incompatibleTarget = method(parameterCount = 1) + val call = Stmt.Call( + method = callMethod, + lValue = null, + instance = null, + args = emptyList(), + originalIdx = 0, + ) + val resolver = object : CallResolver { + override fun resolveMethodCall( + callStmt: Stmt.Call, + level: Int, + ): List = listOf(incompatibleTarget) + + override fun buildMethodGraph(method: JIRMethod): JIRInstGraph = + error("An incompatible target must be rejected before its graph is built") + + override fun externalCallModel( + method: JIRMethod, + ): List = emptyList() + } + val node = CallTreeNode(ContextInfo.rootContext, unusedInstEvalContext) + + val resolved: Map? = node.resolveCall(call, resolver) + + assertNull(resolved) + } + + private val unusedInstEvalContext = object : InstEvalContext { + override fun createThis(isOuter: Boolean): Value = error("unused") + override fun createArg(idx: Int): Value = error("unused") + override fun createLocal(idx: Int): Local = error("unused") + } + + private fun method(parameterCount: Int): JIRMethod { + val type = JIRMethod::class.java + return Proxy.newProxyInstance(type.classLoader, arrayOf(type)) { proxy, invoked, args -> + when (invoked.name) { + "getParameters" -> List(parameterCount) { null } + "equals" -> proxy === args?.singleOrNull() + "hashCode" -> System.identityHashCode(proxy) + "toString" -> "method(arity=$parameterCount)" + else -> error("Unexpected JIRMethod member: ${invoked.name}") + } + } as JIRMethod + } +} From 1244158f6d5c2a3165329be3e47632e8e5156d1e Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Wed, 29 Jul 2026 03:46:30 +0200 Subject: [PATCH 3/4] Model invokedynamic separately from method calls --- .../samples/build.gradle.kts | 7 +- .../java/sample/alias/RecordAliasSample.java | 21 +---- .../jvm/ap/ifds/JIRCallPositionResolver.kt | 22 ++++- .../dataflow/jvm/ap/ifds/JIRCallResolver.kt | 17 ++-- .../jvm/ap/ifds/JIRLanguageManager.kt | 6 +- .../jvm/ap/ifds/alias/DSUAliasAnalysis.kt | 12 ++- .../jvm/ap/ifds/alias/InterProcCallNode.kt | 15 ++-- .../jvm/ap/ifds/alias/JIRDSUAABuilder.kt | 33 +++++++- .../ap/ifds/analysis/JIRAnalysisManager.kt | 23 ++--- .../ifds/analysis/JIRMethodAnalysisContext.kt | 9 +- .../analysis/JIRMethodCallFlowFunction.kt | 10 +-- .../JIRMethodCallRuleBasedSummaryRewriter.kt | 8 +- .../analysis/JIRNonMethodCallFlowFunction.kt | 84 +++++++++++++++++++ .../ap/ifds/taint/JIRBasicAtomEvaluator.kt | 6 +- .../ap/ifds/taint/JIRMethodCallTaintUtil.kt | 3 +- .../ap/ifds/taint/JIRTaintAnalysisContext.kt | 76 ++++++++++------- .../ifds/trace/JIRMethodCallPrecondition.kt | 4 +- .../trace/JIRNonMethodCallPrecondition.kt | 29 +++++++ .../dataflow/jvm/util/JirMethodExt.kt | 4 +- .../opentaint/dataflow/jvm/util/JirVararg.kt | 4 +- .../jvm/ap/ifds/alias/AliasSampleTest.kt | 41 +++++---- ...sAnalysisInvalidateOuterHeapAliasesTest.kt | 2 +- .../ap/ifds/alias/InterProcCallNodeTest.kt | 62 -------------- .../org/opentaint/ir/api/jvm/cfg/JIRInst.kt | 22 +++-- .../org/opentaint/ir/impl/cfg/GraphExt.kt | 6 +- .../ir/testing/UnknownClassesTest.kt | 3 +- .../ir/testing/cfg/InstructionsTest.kt | 12 ++- .../pattern/SemgrepJavaPatternMatcher.kt | 3 +- .../jvm/graph/JApplicationGraphImpl.kt | 5 +- .../transformer/JStringConcatTransformer.kt | 2 +- .../jvm/sast/ast/AbstractAstSpanResolver.kt | 5 +- .../jvm/sast/ast/JavaAstSpanResolver.kt | 5 +- .../jvm/sast/ast/KotlinAstSpanResolver.kt | 6 +- .../JavaPropertiesResolveTransformer.kt | 11 +-- .../SpringReactorOperatorsTransformer.kt | 12 +-- .../jvm/sast/sarif/JIRSarifTraits.kt | 7 +- .../jvm/sast/sarif/TraceMessageBuilder.kt | 9 +- .../jvm/sast/ast/JavaAstSpanResolverTest.kt | 45 +++++----- .../jvm/sast/ast/KotlinAstSpanResolverTest.kt | 36 ++++---- 39 files changed, 406 insertions(+), 281 deletions(-) create mode 100644 core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRNonMethodCallFlowFunction.kt create mode 100644 core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRNonMethodCallPrecondition.kt delete mode 100644 core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNodeTest.kt diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts index 0f080962f..1d04df037 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts @@ -4,11 +4,8 @@ plugins { tasks { withType { - // Java 17 (was 1.8) so record samples compile: records lower equals/ - // hashCode/toString to invokedynamic bootstrapped by - // java.lang.runtime.ObjectMethods, which RecordAliasSample exercises to - // pin the arity guard in resolveCallNoCache. Existing samples use no - // invokedynamic constructs, so their bytecode shape is unchanged. + // Records provide a real unresolved invokedynamic call site for the + // alias-analysis samples. Existing samples remain source-compatible. sourceCompatibility = JavaVersion.VERSION_17.toString() targetCompatibility = JavaVersion.VERSION_17.toString() options.compilerArgs.add("-g") diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/RecordAliasSample.java b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/RecordAliasSample.java index 01294da9a..e0aa09418 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/RecordAliasSample.java +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/src/main/java/sample/alias/RecordAliasSample.java @@ -1,27 +1,14 @@ package sample.alias; -// Records lower equals/hashCode/toString to an invokedynamic bootstrapped by -// java.lang.runtime.ObjectMethods.bootstrap, which declares 6 parameters while -// the dynamic call site supplies one (the record instance). When alias analysis -// inlines the record method and then its bootstrap, mapping the 6 parameters -// onto the single call argument reads arguments the call never provided. This -// sample reproduces exactly that shape so the arity guard in resolveCallNoCache -// can be pinned: without the guard the analysis crashes with -// "Incorrect argument idx"; with it the bootstrap call is treated as opaque. public class RecordAliasSample { public record Payload(Object value) {} static void recordHashCodeInlined(Object src) { - Payload p = new Payload(src); - // p.hashCode() is a record method whose body is the ObjectMethods - // invokedynamic; alias analysis inlines it (and the bootstrap) at depth 2. - p.hashCode(); - // echo is a local var so the test can query its aliases (which triggers - // the full alias computation, and with it the record-method inlining). - Object echo = p.value(); - sinkOneValue(echo); + Payload payload = new Payload(src); + payload.hashCode(); + sinkOneValue(payload.value()); } - static void sinkOneValue(Object v) { } + static void sinkOneValue(Object value) {} } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallPositionResolver.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallPositionResolver.kt index 95a72a46a..091f04038 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallPositionResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallPositionResolver.kt @@ -20,9 +20,9 @@ import org.opentaint.ir.api.jvm.JIRMethod import org.opentaint.ir.api.jvm.JIRParameter import org.opentaint.ir.api.jvm.JIRType import org.opentaint.ir.api.jvm.cfg.JIRArgument -import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRImmediate import org.opentaint.ir.api.jvm.cfg.JIRInstanceCallExpr +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRValue import org.opentaint.ir.api.jvm.ext.toType @@ -33,7 +33,7 @@ sealed interface CallPositionValue { } class CallPositionToJIRValueResolver( - private val callExpr: JIRCallExpr, + private val callExpr: JIRMethodCallExpr, private val returnValue: JIRImmediate? ) : PositionResolver { override fun resolve(position: Position): CallPositionValue = when (position) { @@ -48,6 +48,24 @@ class CallPositionToJIRValueResolver( } } +class JIRMethodCallPositionBaseTypeResolver( + private val callExpr: JIRMethodCallExpr +) : PositionTypeResolver { + override fun resolve(position: PositionAccess): CommonType? { + if (position !is PositionAccess.Simple) return null + + return when (val base = position.base) { + is AccessPathBase.Argument -> callExpr.args.getOrNull(base.idx)?.type + is AccessPathBase.Return -> callExpr.type + is AccessPathBase.This -> (callExpr as? JIRInstanceCallExpr)?.instance?.type + is AccessPathBase.ClassStatic, + is AccessPathBase.Constant, + is AccessPathBase.Exception, + is AccessPathBase.LocalVar -> null + } + } +} + class CalleePositionToJIRValueResolver( private val method: JIRMethod ) : PositionResolver { diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallResolver.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallResolver.kt index ab6174563..777ef8da4 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRCallResolver.kt @@ -27,6 +27,7 @@ import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRInstanceCallExpr import org.opentaint.ir.api.jvm.cfg.JIRLambdaExpr import org.opentaint.ir.api.jvm.cfg.JIRLocalVar +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRNewExpr import org.opentaint.ir.api.jvm.cfg.JIRValue import org.opentaint.ir.api.jvm.cfg.JIRVirtualCallExpr @@ -88,18 +89,22 @@ class JIRCallResolver( } fun resolve(call: JIRCallExpr, location: JIRInst, context: JIRMethodAnalysisContext): List { - val method = call.method.method + if (call is JIRLambdaExpr) { + // lambda expr is an allocation site. lambda calls resolved as virtual calls + return emptyList() + } + + // A bootstrap method links an invokedynamic call site; it is not the + // method executed when the surrounding program reaches that instruction. + val methodCall = call as? JIRMethodCallExpr + ?: return listOf(MethodResolutionResult.MethodResolutionFailed) + val method = methodCall.method.method val methodIgnored = unitResolver.resolve(method) == UnknownUnit if (methodIgnored && alwaysIgnoreMethod(method)) { return listOf(MethodResolutionResult.MethodResolutionFailed) } - if (call is JIRLambdaExpr) { - // lambda expr is an allocation site. lambda calls resolved as virtual calls - return emptyList() - } - if (call !is JIRVirtualCallExpr) { if (methodIgnored) { return listOf(MethodResolutionResult.MethodResolutionFailed) diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRLanguageManager.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRLanguageManager.kt index 50a04b315..6f6cd81e7 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRLanguageManager.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRLanguageManager.kt @@ -8,6 +8,7 @@ import org.opentaint.ir.api.jvm.JIRClasspath import org.opentaint.ir.api.jvm.JIRMethod import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRThrowInst import org.opentaint.ir.api.jvm.cfg.JIRValue import org.opentaint.ir.api.jvm.ext.cfg.callExpr @@ -48,7 +49,8 @@ open class JIRLanguageManager(val cp: JIRClasspath) : LanguageManager { override fun getCalleeMethod(callExpr: CommonCallExpr): JIRMethod { jIRDowncast(callExpr) - return callExpr.method.method + return (callExpr as? JIRMethodCallExpr)?.method?.method + ?: error("Dynamic call sites do not have a callee method") } override val methodContextSerializer = JIRMethodContextSerializer(cp) @@ -60,4 +62,4 @@ internal inline fun jIRDowncast(value: Any?) { returns() implies(value is T) } check(value is T) { "Downcast error: expected ${T::class}, got $value" } -} \ No newline at end of file +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysis.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysis.kt index 341990e74..87047b366 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysis.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysis.kt @@ -150,16 +150,18 @@ class DSUAliasAnalysis( private fun evalCall(stmt: Stmt.Call, state: State, callFrame: CallTreeNode): State { // todo: use instance alloc info - val resolvedCall = callFrame.resolveCall(stmt, methodCallResolver) + val resolvedCall = (stmt as? Stmt.MethodCall)?.let { + callFrame.resolveCall(it, methodCallResolver) + } if (resolvedCall != null) { val result = evalCall(stmt, state, callFrame, resolvedCall) if (result != null) return result } var resultState = state - if (stmt.lValue != null) { + stmt.lValue?.let { lValue -> val info = aliasSetFromInfo(CallReturn(stmt, callFrame.ctx)) - resultState = resultState.removeOldAndMergeWith(stmt.lValue.aliasInfo().index(), info) + resultState = resultState.removeOldAndMergeWith(lValue.aliasInfo().index(), info) } if (!stmt.cantMutateAliasedHeap()) { @@ -172,7 +174,8 @@ class DSUAliasAnalysis( resultState = resultState.invalidateOuterHeapAliases(argAliases) } - val externalModel = methodCallResolver.externalCallModel(stmt.method) + val method = (stmt as? Stmt.MethodCall)?.method + val externalModel = method?.let(methodCallResolver::externalCallModel).orEmpty() resultState = externalModel.fold(resultState) { s, model -> model.evalExternalCallModel(stmt, s) } @@ -566,6 +569,7 @@ class DSUAliasAnalysis( private fun Stmt.Call.cantMutateAliasedHeap(): Boolean { if (args.any { it !is SimpleValue.Primitive }) return false + val method = (this as? Stmt.MethodCall)?.method ?: return false return method.isStatic || method.isConstructor } } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt index 9c16af16e..0674246f0 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNode.kt @@ -15,7 +15,7 @@ import org.opentaint.jvm.graph.JApplicationGraph import java.util.BitSet interface CallResolver { - fun resolveMethodCall(callStmt: Stmt.Call, level: Int): List? + fun resolveMethodCall(callStmt: Stmt.MethodCall, level: Int): List? fun buildMethodGraph(method: JIRMethod): JIRInstGraph? fun externalCallModel(method: JIRMethod): List } @@ -28,7 +28,7 @@ abstract class JirCallResolver( ): CallResolver { abstract fun buildMethodJig(entryPoint: JIRInst): JIRInstGraph - override fun resolveMethodCall(callStmt: Stmt.Call, level: Int): List? { + override fun resolveMethodCall(callStmt: Stmt.MethodCall, level: Int): List? { if (level >= params.aliasAnalysisInterProcCallDepth) return null val methods = callResolver.allKnownOverridesOrNull(callStmt.method) @@ -56,7 +56,7 @@ class CallTreeNode(val ctx: ContextInfo, val instEvalCtx: InstEvalContext) { private val emptyCalls = BitSet() private val calls = Int2ObjectOpenHashMap() - fun resolveCall(stmt: Stmt.Call, callResolver: CallResolver): Map? { + fun resolveCall(stmt: Stmt.MethodCall, callResolver: CallResolver): Map? { if (emptyCalls.get(stmt.originalIdx)) return ResolvedCall.empty.methods return calls.getOrPut(stmt.originalIdx) { @@ -87,16 +87,11 @@ private class NestedCallInstEvalCtx(val call: Stmt.Call, val ctx: ContextInfo) : override fun createLocal(idx: Int): Local = Local(idx, ctx) } -private fun resolveCallNoCache(stmt: Stmt.Call, ctx: ContextInfo, callResolver: CallResolver): ResolvedCall { +private fun resolveCallNoCache(stmt: Stmt.MethodCall, ctx: ContextInfo, callResolver: CallResolver): ResolvedCall { val methods = callResolver.resolveMethodCall(stmt, ctx.level) ?: return ResolvedCall.empty val resolvedCall = methods.mapIndexedNotNull { idx, method -> - // Override approximation may return a bridge or synthetic target with a different - // descriptor. Such a method cannot be the target of this call and must not enter the - // nested analysis with arguments mapped to the wrong positions. - if (method.parameters.size != stmt.args.size) return@mapIndexedNotNull null - val graph = callResolver.buildMethodGraph(method) ?: return@mapIndexedNotNull null @@ -111,5 +106,5 @@ private fun resolveCallNoCache(stmt: Stmt.Call, ctx: ContextInfo, callResolver: return ResolvedCall(resolvedCall) } -private fun mkContextId(stmt: Stmt.Call, methodIdx: Int): Int = +private fun mkContextId(stmt: Stmt.MethodCall, methodIdx: Int): Int = (stmt.originalIdx * 1000) + methodIdx diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/JIRDSUAABuilder.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/JIRDSUAABuilder.kt index 5fc25ffb9..51826ac86 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/JIRDSUAABuilder.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/JIRDSUAABuilder.kt @@ -18,6 +18,7 @@ import org.opentaint.ir.api.jvm.cfg.JIRImmediate import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRInstanceCallExpr import org.opentaint.ir.api.jvm.cfg.JIRLocalVar +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRNewArrayExpr import org.opentaint.ir.api.jvm.cfg.JIRNewExpr import org.opentaint.ir.api.jvm.cfg.JIRRef @@ -91,7 +92,26 @@ sealed interface Stmt : Comparable { sealed interface NoCall: Stmt - data class Call(val method: JIRMethod, val lValue: RefValue.Local?, val instance: Value?, val args: List, override val originalIdx: Int) : Stmt + sealed interface Call : Stmt { + val lValue: RefValue.Local? + val instance: Value? + val args: List + } + + data class MethodCall( + val method: JIRMethod, + override val lValue: RefValue.Local?, + override val instance: Value?, + override val args: List, + override val originalIdx: Int + ) : Call + + data class OpaqueCall( + override val lValue: RefValue.Local?, + override val instance: Value?, + override val args: List, + override val originalIdx: Int + ) : Call data class Copy(val lValue: RefValue.Local, val rValue: RefValue, override val originalIdx: Int): NoCall data class Assign(val lValue: RefValue.Local, val expr: Expr, override val originalIdx: Int) : NoCall @@ -192,15 +212,20 @@ private fun InstEvalContext.evalCall( ): Stmt? { val lhs = (lValue as? JIRLocalVar)?.let { createLocal(it.index) } - if (expr.method.method.isPrimitiveBoxAllocMethod()) { + val method = (expr as? JIRMethodCallExpr)?.method?.method + if (method?.isPrimitiveBoxAllocMethod() == true) { if (lhs == null) return null return Stmt.Assign(lhs, Expr.Alloc(loc), loc.location.index) } val args = expr.args.map { evalSimpleValue(it as JIRImmediate, loc) } val instance = (expr as? JIRInstanceCallExpr)?.instance?.let { evalSimpleValue(it as JIRImmediate, loc) } - val stmt = Stmt.Call(expr.method.method, lhs, instance, args, loc.location.index) - return stmt + return when (expr) { + is JIRMethodCallExpr -> Stmt.MethodCall( + expr.method.method, lhs, instance, args, loc.location.index + ) + else -> Stmt.OpaqueCall(lhs, instance, args, loc.location.index) + } } private fun InstEvalContext.evalExpr(expr: JIRExpr, inst: JIRInst): ExprOrValue = when (expr) { diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRAnalysisManager.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRAnalysisManager.kt index adc8d8297..f5c542184 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRAnalysisManager.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRAnalysisManager.kt @@ -36,6 +36,7 @@ import org.opentaint.dataflow.jvm.ap.ifds.jIRDowncast import org.opentaint.dataflow.jvm.ap.ifds.taint.JIRTaintAnalysisContext import org.opentaint.dataflow.jvm.ap.ifds.taint.TaintRulesProvider import org.opentaint.dataflow.jvm.ap.ifds.trace.JIRMethodCallPrecondition +import org.opentaint.dataflow.jvm.ap.ifds.trace.JIRNonMethodCallPrecondition import org.opentaint.dataflow.jvm.ap.ifds.trace.JIRMethodSequentPrecondition import org.opentaint.dataflow.jvm.ap.ifds.trace.JIRMethodStartPrecondition import org.opentaint.dataflow.jvm.ifds.JIRUnitResolver @@ -48,6 +49,7 @@ import org.opentaint.ir.api.jvm.JIRClasspath import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRImmediate import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.jvm.graph.JApplicationGraph import org.opentaint.util.analysis.ApplicationGraph import java.util.concurrent.ConcurrentHashMap @@ -211,13 +213,11 @@ class JIRAnalysisManager( jIRDowncast(analysisContext) return analysisContext.cachedCallFF(statement.location.index) { + val methodCall = callExpr as? JIRMethodCallExpr + ?: return@cachedCallFF JIRNonMethodCallFlowFunction(returnValue, callExpr) + JIRMethodCallFlowFunction( - apManager, - analysisContext, - returnValue, - callExpr, - statement, - generateTrace + apManager, analysisContext, returnValue, methodCall, statement, generateTrace ) } } @@ -259,13 +259,8 @@ class JIRAnalysisManager( jIRDowncast(statement) jIRDowncast(analysisContext) - return JIRMethodCallPrecondition( - apManager, - analysisContext, - returnValue, - callExpr, - statement - ) + val methodCall = callExpr as? JIRMethodCallExpr ?: return JIRNonMethodCallPrecondition + return JIRMethodCallPrecondition(apManager, analysisContext, returnValue, methodCall, statement) } override fun getEdgePostProcessor( @@ -327,4 +322,4 @@ class JIRAnalysisManager( val percentValue = current.toDouble() / total return String.format("%.2f", percentValue * 100) + "%" } -} \ No newline at end of file +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodAnalysisContext.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodAnalysisContext.kt index d9837eac0..6fb9cdb8e 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodAnalysisContext.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodAnalysisContext.kt @@ -6,6 +6,7 @@ import org.opentaint.dataflow.ap.ifds.TaintAnalysisManager.Phase import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor import org.opentaint.dataflow.ap.ifds.analysis.MethodAnalysisContext import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFactMapper +import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction import org.opentaint.dataflow.jvm.ap.ifds.JIRFactTypeChecker import org.opentaint.dataflow.jvm.ap.ifds.JIRLambdaTracker import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis @@ -38,16 +39,16 @@ class JIRMethodAnalysisContext( val lambdaCallResolution = Int2ObjectOpenHashMap() - fun cachedCallFF(stmtIdx: Int, body: () -> JIRMethodCallFlowFunction): JIRMethodCallFlowFunction = + fun cachedCallFF(stmtIdx: Int, body: () -> MethodCallFlowFunction): MethodCallFlowFunction = getCallFFCache().computeIfAbsent(stmtIdx) { body() } fun cachedCallSH(stmtIdx: Int, body: () -> JIRMethodCallSummaryHandler): JIRMethodCallSummaryHandler = getCallSHCache().computeIfAbsent(stmtIdx) { body() } - private var callFFCache: Reference>? = null - private fun getCallFFCache(): Int2ObjectOpenHashMap { + private var callFFCache: Reference>? = null + private fun getCallFFCache(): Int2ObjectOpenHashMap { callFFCache?.get()?.let { return it } - return int2ObjectMap().also { + return int2ObjectMap().also { callFFCache = refManager.createRef(it) } } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt index 81c6b6ee0..34c6b4883 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt @@ -17,7 +17,7 @@ import org.opentaint.dataflow.configuration.jvm.serialized.UserDefinedRuleInfo import org.opentaint.dataflow.jvm.ap.ifds.JIRCallResolver import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodCallFactMapper import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodCallFactMapper.factIsRelevantToMethodCall -import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodPositionBaseTypeResolver +import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodCallPositionBaseTypeResolver import org.opentaint.dataflow.jvm.ap.ifds.TaintConfigUtils.applyCleaner import org.opentaint.dataflow.jvm.ap.ifds.TaintConfigUtils.applyPassThrough import org.opentaint.dataflow.jvm.ap.ifds.taint.JIRMethodCallTaintUtil @@ -29,16 +29,16 @@ import org.opentaint.dataflow.taint.FinalFactReader import org.opentaint.dataflow.taint.TaintFactAwareConditionEvaluator import org.opentaint.dataflow.taint.TaintPassActionEvaluator import org.opentaint.ir.api.jvm.JIRMethod -import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRImmediate import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.util.onSome class JIRMethodCallFlowFunction( private val apManager: ApManager, private val analysisContext: JIRMethodAnalysisContext, private val returnValue: JIRImmediate?, - private val callExpr: JIRCallExpr, + private val callExpr: JIRMethodCallExpr, private val statement: JIRInst, private val generateTrace: Boolean, ): MethodCallFlowFunction.Default { @@ -49,7 +49,7 @@ class JIRMethodCallFlowFunction( } val typeResolver by lazy { - JIRMethodPositionBaseTypeResolver(callExpr.method.method) + JIRMethodCallPositionBaseTypeResolver(callExpr) } override fun propagateZeroToZero() = buildSet { @@ -223,7 +223,6 @@ class JIRMethodCallFlowFunction( ): List> { val sinkRules = taintCtx.sinkRulesForCallStatement(statement, callExpr, returnValue, factReader?.factAp) if (sinkRules.isEmpty()) return emptyList() - val taintUtil = JIRMethodCallTaintUtil(apManager, statement, callExpr, analysisContext, generateTrace) taintUtil.applySinkRules( sinkRules, factReader, markAfterAnyFieldResolver @@ -241,7 +240,6 @@ class JIRMethodCallFlowFunction( ) { val sourceRules = taintCtx.sourceRulesForCallStatement(statement, callExpr, returnValue, factReader?.factAp) if (sourceRules.isEmpty()) return - val taintUtil = JIRMethodCallTaintUtil(apManager, statement, callExpr, analysisContext, generateTrace) taintUtil.applySourceRules( sourceRules, initialFacts, factReader, exclusion, diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt index 3aec7b58e..b7c9dee67 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt @@ -10,7 +10,7 @@ import org.opentaint.dataflow.configuration.jvm.TaintMark import org.opentaint.dataflow.configuration.jvm.serialized.UserDefinedRuleInfo import org.opentaint.dataflow.jvm.ap.ifds.CallPositionToJIRValueResolver import org.opentaint.dataflow.jvm.ap.ifds.JIRMarkAwareConditionRewriter -import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodPositionBaseTypeResolver +import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodCallPositionBaseTypeResolver import org.opentaint.dataflow.jvm.ap.ifds.TaintConfigUtils.applyCleanerActions import org.opentaint.dataflow.jvm.ap.ifds.taint.JIRTaintCleanActionEvaluator import org.opentaint.dataflow.jvm.ap.ifds.taint.resolveBaseAp @@ -19,6 +19,7 @@ import org.opentaint.dataflow.taint.FinalFactReader import org.opentaint.ir.api.jvm.cfg.JIRAssignInst import org.opentaint.ir.api.jvm.cfg.JIRImmediate import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.ext.cfg.callExpr class JIRMethodCallRuleBasedSummaryRewriter( @@ -29,7 +30,8 @@ class JIRMethodCallRuleBasedSummaryRewriter( private val taintCtx get() = analysisContext.taint private val callExpr by lazy { - statement.callExpr ?: error("Call summary handler at statement without method call") + statement.callExpr as? JIRMethodCallExpr + ?: error("Method-call summary handler at a non-method call site") } private val conditionRewriter by lazy { @@ -42,7 +44,7 @@ class JIRMethodCallRuleBasedSummaryRewriter( } private val typeResolver by lazy { - JIRMethodPositionBaseTypeResolver(callExpr.method.method) + JIRMethodCallPositionBaseTypeResolver(callExpr) } private data class UserRuleDefinedAction( diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRNonMethodCallFlowFunction.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRNonMethodCallFlowFunction.kt new file mode 100644 index 000000000..9a4cf625c --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRNonMethodCallFlowFunction.kt @@ -0,0 +1,84 @@ +package org.opentaint.dataflow.jvm.ap.ifds.analysis + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.access.FactAp +import org.opentaint.dataflow.ap.ifds.access.FinalFactAp +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction +import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction.CallToReturnFFact +import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction.CallToReturnNonDistributiveFact +import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction.CallToReturnZFact +import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction.CallToReturnZeroFact +import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction.TraceInfo +import org.opentaint.dataflow.ap.ifds.analysis.MethodCallFlowFunction.Unchanged +import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodCallFactMapper +import org.opentaint.ir.api.jvm.cfg.JIRCallExpr +import org.opentaint.ir.api.jvm.cfg.JIRImmediate + +/** + * Call-to-return flow for call-like expressions that do not invoke a method. + * + * Lambda expressions construct an object. An invokedynamic bootstrap links its call site instead + * of being invoked by the program. Neither expression has a callee to analyze or match against + * method rules, summaries, and approximations. + */ +class JIRNonMethodCallFlowFunction( + private val returnValue: JIRImmediate?, + private val callExpr: JIRCallExpr, +) : MethodCallFlowFunction { + override fun propagateZeroToZero(): Set = + setOf(CallToReturnZeroFact) + + override fun propagateZeroToFact(currentFactAp: FinalFactAp): Set = + if (isRelevant(currentFactAp)) { + setOf(CallToReturnZFact(currentFactAp, TraceInfo.Flow)) + } else { + setOf(Unchanged) + } + + override fun propagateFactToFact( + initialFactAp: InitialFactAp, + currentFactAp: FinalFactAp, + ): Set = + if (isRelevant(currentFactAp)) { + setOf(CallToReturnFFact(initialFactAp, currentFactAp, TraceInfo.Flow)) + } else { + setOf(Unchanged) + } + + override fun propagateNDFactToFact( + initialFacts: Set, + currentFactAp: FinalFactAp, + ): Set = + if (isRelevant(currentFactAp)) { + setOf(CallToReturnNonDistributiveFact(initialFacts, currentFactAp, TraceInfo.Flow)) + } else { + setOf(Unchanged) + } + + override fun propagateZeroToZeroResolutionFailure(): Set = + setOf(CallToReturnZeroFact) + + override fun propagateZeroToFactResolutionFailure( + currentFactAp: FinalFactAp, + startFactBase: AccessPathBase, + ): Set = + setOf(CallToReturnZFact(currentFactAp, TraceInfo.Flow)) + + override fun propagateFactToFactResolutionFailure( + initialFactAp: InitialFactAp, + currentFactAp: FinalFactAp, + startFactBase: AccessPathBase, + ): Set = + setOf(CallToReturnFFact(initialFactAp, currentFactAp, TraceInfo.Flow)) + + override fun propagateNDFactToFactResolutionFailure( + initialFacts: Set, + currentFactAp: FinalFactAp, + startFactBase: AccessPathBase, + ): Set = + setOf(CallToReturnNonDistributiveFact(initialFacts, currentFactAp, TraceInfo.Flow)) + + private fun isRelevant(fact: FactAp): Boolean = + JIRMethodCallFactMapper.factIsRelevantToMethodCall(returnValue, callExpr, fact.base) +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRBasicAtomEvaluator.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRBasicAtomEvaluator.kt index 77f1b240a..70952dd2e 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRBasicAtomEvaluator.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRBasicAtomEvaluator.kt @@ -35,12 +35,12 @@ import org.opentaint.ir.api.jvm.JIRClassType import org.opentaint.ir.api.jvm.JIRRefType import org.opentaint.ir.api.jvm.JIRType import org.opentaint.ir.api.jvm.cfg.JIRBool -import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRConstant import org.opentaint.ir.api.jvm.cfg.JIRFieldRef import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRInt import org.opentaint.ir.api.jvm.cfg.JIRLocalVar +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRNullConstant import org.opentaint.ir.api.jvm.cfg.JIRStringConstant import org.opentaint.ir.api.jvm.cfg.JIRValue @@ -226,14 +226,14 @@ class JIRBasicAtomEvaluator( } .mapNotNull { it.args.getOrNull(0) as? JIRConstant } - private fun List.findAllocCalls(): List { + private fun List.findAllocCalls(): List { val allocs = filterIsInstance() if (allocs.isEmpty()) return emptyList() val instList = (statement as JIRInst).location.method.instList return allocs .mapNotNull { instList.getOrNull(it.allocInst) } - .mapNotNull { it.callExpr } + .mapNotNull { it.callExpr as? JIRMethodCallExpr } } private fun matches(value: JIRValue, pattern: Regex, matchArrayValue: Boolean): Boolean { diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRMethodCallTaintUtil.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRMethodCallTaintUtil.kt index 393db42f4..f2fda92be 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRMethodCallTaintUtil.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRMethodCallTaintUtil.kt @@ -24,12 +24,13 @@ import org.opentaint.dataflow.taint.TaintSourceActionEvaluator import org.opentaint.dataflow.taint.TaintUtil import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.util.onSome class JIRMethodCallTaintUtil( apManager: ApManager, val statement: JIRInst, - val callExpr: JIRCallExpr, + val callExpr: JIRMethodCallExpr, val analysisContext: JIRMethodAnalysisContext, val generateTrace: Boolean, ) : TaintUtil(apManager) { diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRTaintAnalysisContext.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRTaintAnalysisContext.kt index d5311836c..553d6b011 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRTaintAnalysisContext.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRTaintAnalysisContext.kt @@ -24,10 +24,9 @@ import org.opentaint.dataflow.jvm.ap.ifds.JIRMarkAwareConditionRewriter import org.opentaint.dataflow.jvm.ap.ifds.analysis.JIRMethodAnalysisContext import org.opentaint.dataflow.taint.RuleConditionRewriter import org.opentaint.ir.api.jvm.JIRField -import org.opentaint.ir.api.jvm.JIRMethod -import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRImmediate import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.ext.cfg.callExpr class JIRTaintAnalysisContext( @@ -46,67 +45,80 @@ class JIRTaintAnalysisContext( taintSinkTracker.reset() } - private fun JIRInst.callExpr(): JIRCallExpr = callExpr ?: error("Non-call statement") - private fun JIRCallExpr.calleeMethod(): JIRMethod = method.method - private fun JIRInst.calleeMethod(): JIRMethod = callExpr().calleeMethod() + private fun JIRInst.methodCallExprOrNull(): JIRMethodCallExpr? = + callExpr as? JIRMethodCallExpr fun allRelevantSourceRulesForCallStatement(statement: JIRInst): Iterable { if (analysisContext.phase is Phase.Prescan) return emptyList() - return taintConfig.sourceRulesForMethod(statement.calleeMethod(), statement, fact = null, allRelevant = true) + val method = statement.methodCallExprOrNull()?.method?.method ?: return emptyList() + return taintConfig.sourceRulesForMethod(method, statement, fact = null, allRelevant = true) } fun allRelevantCleanRulesForCallStatement(statement: JIRInst): Iterable { if (analysisContext.phase is Phase.Prescan) return emptyList() - return taintConfig.cleanerRulesForMethod(statement.calleeMethod(), statement, fact = null, allRelevant = true) + val method = statement.methodCallExprOrNull()?.method?.method ?: return emptyList() + return taintConfig.cleanerRulesForMethod(method, statement, fact = null, allRelevant = true) } fun sourceRulesForCallStatement( statement: JIRInst, - callExpr: JIRCallExpr, + callExpr: JIRMethodCallExpr, returnValue: JIRImmediate?, fact: FinalFactAp? - ) = prepareCallStatementRules( - taintConfig.sourceRulesForMethod(statement.calleeMethod(), statement, fact, allRelevant = false), - TaintMethodSource::condition, - statement, callExpr, returnValue - ) + ): List> { + val method = callExpr.method.method + return prepareCallStatementRules( + taintConfig.sourceRulesForMethod(method, statement, fact, allRelevant = false), + TaintMethodSource::condition, + statement, callExpr, returnValue + ) + } fun sinkRulesForCallStatement( statement: JIRInst, - callExpr: JIRCallExpr, + callExpr: JIRMethodCallExpr, returnValue: JIRImmediate?, fact: FinalFactAp? - ) = prepareCallStatementRules( - taintConfig.sinkRulesForMethod(statement.calleeMethod(), statement, fact, allRelevant = false), - TaintMethodSink::condition, - statement, callExpr, returnValue - ) + ): List> { + val method = callExpr.method.method + return prepareCallStatementRules( + taintConfig.sinkRulesForMethod(method, statement, fact, allRelevant = false), + TaintMethodSink::condition, + statement, callExpr, returnValue + ) + } fun cleanRulesForCallStatement( statement: JIRInst, - callExpr: JIRCallExpr, + callExpr: JIRMethodCallExpr, returnValue: JIRImmediate?, fact: FinalFactAp? - ) = prepareCallStatementRules( - taintConfig.cleanerRulesForMethod(statement.calleeMethod(), statement, fact, allRelevant = false), - TaintCleaner::condition, - statement, callExpr, returnValue - ) + ): List> { + val method = callExpr.method.method + return prepareCallStatementRules( + taintConfig.cleanerRulesForMethod(method, statement, fact, allRelevant = false), + TaintCleaner::condition, + statement, callExpr, returnValue + ) + } fun passRulesForCallStatement( statement: JIRInst, - callExpr: JIRCallExpr, + callExpr: JIRMethodCallExpr, returnValue: JIRImmediate?, fact: FinalFactAp? - ) = prepareCallStatementRules( - taintConfig.passTroughRulesForMethod(statement.calleeMethod(), statement, fact, allRelevant = false), - TaintPassThrough::condition, - statement, callExpr, returnValue - ) + ): List> { + val method = callExpr.method.method + return prepareCallStatementRules( + taintConfig.passTroughRulesForMethod(method, statement, fact, allRelevant = false), + TaintPassThrough::condition, + statement, callExpr, returnValue + ) + } private inline fun prepareCallStatementRules( rules: Iterable, cond: T.() -> Condition, - statement: JIRInst, callExpr: JIRCallExpr, returnValue: JIRImmediate?, + statement: JIRInst, callExpr: JIRMethodCallExpr, returnValue: JIRImmediate?, ): List> { val conditionRewriter = JIRMarkAwareConditionRewriter( CallPositionToJIRValueResolver(callExpr, returnValue), diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodCallPrecondition.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodCallPrecondition.kt index 365e722c1..b1eda3d6c 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodCallPrecondition.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRMethodCallPrecondition.kt @@ -28,15 +28,15 @@ import org.opentaint.dataflow.taint.TaintSourceActionPreconditionEvaluator import org.opentaint.dataflow.taint.evaluatePassRulePrecondition import org.opentaint.dataflow.taint.evaluateSourceRulePrecondition import org.opentaint.ir.api.common.cfg.CommonInst -import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRImmediate import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr class JIRMethodCallPrecondition( override val apManager: ApManager, private val analysisContext: JIRMethodAnalysisContext, private val returnValue: JIRImmediate?, - private val callExpr: JIRCallExpr, + private val callExpr: JIRMethodCallExpr, private val statement: JIRInst, ) : MethodCallPrecondition.Default { private val methodCallFactMapper: MethodCallFactMapper get() = analysisContext.methodCallFactMapper diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRNonMethodCallPrecondition.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRNonMethodCallPrecondition.kt new file mode 100644 index 000000000..7bdb4aa67 --- /dev/null +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/trace/JIRNonMethodCallPrecondition.kt @@ -0,0 +1,29 @@ +package org.opentaint.dataflow.jvm.ap.ifds.trace + +import org.opentaint.dataflow.ap.ifds.AccessPathBase +import org.opentaint.dataflow.ap.ifds.MethodAnalyzerEdges +import org.opentaint.dataflow.ap.ifds.access.InitialFactAp +import org.opentaint.dataflow.ap.ifds.trace.MethodCallPrecondition +import org.opentaint.dataflow.ap.ifds.trace.MethodCallPrecondition.CallPrecondition +import org.opentaint.dataflow.ap.ifds.trace.MethodCallPrecondition.CallPreconditionFact +import org.opentaint.dataflow.ap.ifds.trace.MethodCallPrecondition.PassRuleConditionFacts +import org.opentaint.dataflow.ap.ifds.trace.TaintRulePrecondition.PassRuleCondition + +/** + * Non-method call sites have no callee summaries or method rules to reconstruct. + */ +object JIRNonMethodCallPrecondition : MethodCallPrecondition { + override fun factPrecondition(fact: InitialFactAp): List = + listOf(CallPrecondition.Unchanged) + + override fun factPreconditionResolutionFailure( + fact: InitialFactAp, + startFactBase: AccessPathBase, + ): List = + listOf(CallPreconditionFact.UnresolvedCallSkip) + + override fun resolvePassRuleCondition( + precondition: PassRuleCondition, + edges: MethodAnalyzerEdges, + ): List = emptyList() +} diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/util/JirMethodExt.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/util/JirMethodExt.kt index 2e5b82f5d..a2cc36575 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/util/JirMethodExt.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/util/JirMethodExt.kt @@ -1,7 +1,7 @@ package org.opentaint.dataflow.jvm.util import org.opentaint.ir.api.jvm.JIRMethod -import org.opentaint.ir.api.jvm.cfg.JIRCallExpr +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRThis import org.opentaint.ir.api.jvm.ext.toType @@ -9,5 +9,5 @@ import org.opentaint.ir.api.jvm.ext.toType val JIRMethod.thisInstance: JIRThis get() = JIRThis(enclosingClass.toType()) -val JIRCallExpr.callee: JIRMethod +val JIRMethodCallExpr.callee: JIRMethod get() = method.method diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/util/JirVararg.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/util/JirVararg.kt index 3106028d7..7b87159f3 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/util/JirVararg.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/util/JirVararg.kt @@ -2,12 +2,12 @@ package org.opentaint.dataflow.jvm.util import org.objectweb.asm.Opcodes import org.opentaint.ir.api.jvm.JIRMethod -import org.opentaint.ir.api.jvm.cfg.JIRCallExpr +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr fun JIRMethod.isVararg(): Boolean = access and Opcodes.ACC_VARARGS != 0 -fun JIRCallExpr.isVararg(): Boolean = +fun JIRMethodCallExpr.isVararg(): Boolean = method.method.isVararg() fun JIRMethod.varargParamIdx(): Int = diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt index 37eafc7f5..64f9de67f 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt @@ -37,12 +37,16 @@ import org.opentaint.ir.api.jvm.JIRField import org.opentaint.ir.api.jvm.JIRMethod import org.opentaint.ir.api.jvm.RegisteredLocation import org.opentaint.ir.api.jvm.cfg.JIRCallInst +import org.opentaint.ir.api.jvm.cfg.JIRDynamicCallExpr +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRLocalVar import org.opentaint.ir.api.jvm.cfg.JIRValue import org.opentaint.ir.impl.features.usagesExt +import org.opentaint.ir.api.jvm.ext.cfg.callExpr import org.opentaint.jvm.graph.JApplicationGraphImpl import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -579,21 +583,28 @@ class AliasSampleTest : BasicTestUtils() { } @Test - fun `record invokedynamic bootstrap does not overflow alias arg mapping`() { + fun `record invokedynamic is not analyzed as a call to its bootstrap method`() { val method = findMethod(RECORD_SAMPLE, "recordHashCodeInlined") + val recordHashCode = cp.findClassOrNull("$RECORD_SAMPLE\$Payload") + ?.declaredMethods + ?.single { it.name == "hashCode" } + ?: error("Record hashCode method not found") + val dynamicInst = recordHashCode.instList.single { it.callExpr is JIRDynamicCallExpr } + val dynamicCall = dynamicInst.callExpr as JIRDynamicCallExpr + + assertEquals("bootstrap", dynamicCall.bootstrapMethod.name) + assertEquals(dynamicCall.callSiteReturnType, dynamicCall.type) + assertFalse { dynamicCall.type == dynamicCall.bootstrapMethod.returnType } + + val usages = runBlocking { cp.usagesExt() } + val graph = JApplicationGraphImpl(cp, usages) + assertTrue { graph.callees(dynamicInst).none() } - // depth 2: recordHashCodeInlined (level 0) -> Payload.hashCode() (level 1) - // -> ObjectMethods.bootstrap (level 2). The record method lowers to an - // invokedynamic whose bootstrap declares 6 parameters, while the dynamic - // call site supplies 1. Querying an alias runs the full computation, which - // inlines that chain. Without the arity guard in resolveCallNoCache the - // nested-call arg mapping indexes past call.args and throws - // IllegalStateException("Incorrect argument idx"); with it the bootstrap - // call is skipped as opaque and the analysis completes. assertDoesNotThrow { val aa = aaForMethodKnowingObjectMethods(method, depth = 2) val sink = method.findSinkCall("sinkOneValue") - aa.sinkArgApAliases(sink) + val aliases = aa.sinkArgApAliases(sink) + assertTrue { aliases.any { it.base == Argument(0) } } } } @@ -617,10 +628,8 @@ class AliasSampleTest : BasicTestUtils() { val usages = runBlocking { cp.usagesExt() } val graph = JApplicationGraphImpl(cp, usages) - // Treat both the sample location and java.lang.runtime.ObjectMethods as - // known so the record method AND its invokedynamic bootstrap resolve to - // inlinable graphs -- reproducing the condition under which the crash - // occurred (a production model where the bootstrap method was resolvable). + // Make the bootstrap implementation available. The test must still not + // enter it: invokedynamic executes its linked call site, not the bootstrap. val sampleLoc = method.enclosingClass.declaration.location val objectMethodsLoc = cp.findClassOrNull("java.lang.runtime.ObjectMethods")?.declaration?.location @@ -639,7 +648,9 @@ class AliasSampleTest : BasicTestUtils() { JIRLocalAliasAnalysis.Params(useAliasAnalysis = true, aliasAnalysisInterProcCallDepth = depth) private fun JIRMethod.findSinkCall(sinkName: String): JIRCallInst = - instList.filterIsInstance().first { it.callExpr.method.name == sinkName } + instList.filterIsInstance().first { + (it.callExpr as? JIRMethodCallExpr)?.method?.name == sinkName + } private fun JIRLocalAliasAnalysis.valueApAliases(value: JIRValue, stmt: JIRInst): List = valueAliases(value, stmt).filterIsInstance() diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt index aefa6cc53..1fc46edfc 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt @@ -17,7 +17,7 @@ class DSUAliasAnalysisInvalidateOuterHeapAliasesTest { private val analysis = DSUAliasAnalysis( methodCallResolver = object : CallResolver { - override fun resolveMethodCall(callStmt: Stmt.Call, level: Int): List? = null + override fun resolveMethodCall(callStmt: Stmt.MethodCall, level: Int): List? = null override fun buildMethodGraph(method: JIRMethod): JIRInstGraph? = null override fun externalCallModel(method: JIRMethod): List = emptyList() }, diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNodeTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNodeTest.kt deleted file mode 100644 index 6fd200864..000000000 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/InterProcCallNodeTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package org.opentaint.dataflow.jvm.ap.ifds.alias - -import org.opentaint.dataflow.ap.ifds.analysis.alias.ContextInfo -import org.opentaint.dataflow.jvm.ap.ifds.alias.DSUAliasAnalysis.ResolvedCallMethod -import org.opentaint.dataflow.jvm.ap.ifds.alias.JIRIntraProcAliasAnalysis.JIRInstGraph -import org.opentaint.dataflow.jvm.ap.ifds.alias.RefValue.Local -import org.opentaint.ir.api.jvm.JIRMethod -import java.lang.reflect.Proxy -import kotlin.test.Test -import kotlin.test.assertNull - -class InterProcCallNodeTest { - @Test - fun `call resolution rejects a target with incompatible arity`() { - val callMethod = method(parameterCount = 0) - val incompatibleTarget = method(parameterCount = 1) - val call = Stmt.Call( - method = callMethod, - lValue = null, - instance = null, - args = emptyList(), - originalIdx = 0, - ) - val resolver = object : CallResolver { - override fun resolveMethodCall( - callStmt: Stmt.Call, - level: Int, - ): List = listOf(incompatibleTarget) - - override fun buildMethodGraph(method: JIRMethod): JIRInstGraph = - error("An incompatible target must be rejected before its graph is built") - - override fun externalCallModel( - method: JIRMethod, - ): List = emptyList() - } - val node = CallTreeNode(ContextInfo.rootContext, unusedInstEvalContext) - - val resolved: Map? = node.resolveCall(call, resolver) - - assertNull(resolved) - } - - private val unusedInstEvalContext = object : InstEvalContext { - override fun createThis(isOuter: Boolean): Value = error("unused") - override fun createArg(idx: Int): Value = error("unused") - override fun createLocal(idx: Int): Local = error("unused") - } - - private fun method(parameterCount: Int): JIRMethod { - val type = JIRMethod::class.java - return Proxy.newProxyInstance(type.classLoader, arrayOf(type)) { proxy, invoked, args -> - when (invoked.name) { - "getParameters" -> List(parameterCount) { null } - "equals" -> proxy === args?.singleOrNull() - "hashCode" -> System.identityHashCode(proxy) - "toString" -> "method(arity=$parameterCount)" - else -> error("Unexpected JIRMethod member: ${invoked.name}") - } - } as JIRMethod - } -} diff --git a/core/opentaint-ir/opentaint-ir-api-jvm/src/main/kotlin/org/opentaint/ir/api/jvm/cfg/JIRInst.kt b/core/opentaint-ir/opentaint-ir-api-jvm/src/main/kotlin/org/opentaint/ir/api/jvm/cfg/JIRInst.kt index fae604986..a3e2bcbe1 100644 --- a/core/opentaint-ir/opentaint-ir-api-jvm/src/main/kotlin/org/opentaint/ir/api/jvm/cfg/JIRInst.kt +++ b/core/opentaint-ir/opentaint-ir-api-jvm/src/main/kotlin/org/opentaint/ir/api/jvm/cfg/JIRInst.kt @@ -585,18 +585,20 @@ data class JIRInstanceOfExpr( } interface JIRCallExpr : JIRExpr, CommonCallExpr { - val method: JIRTypedMethod - override val args: List - override val type: JIRType - get() = method.returnType - override val operands: List get() = args } -interface JIRInstanceCallExpr : JIRCallExpr, CommonInstanceCallExpr { +interface JIRMethodCallExpr : JIRCallExpr { + val method: JIRTypedMethod + + override val type: JIRType + get() = method.returnType +} + +interface JIRInstanceCallExpr : JIRMethodCallExpr, CommonInstanceCallExpr { override val instance: JIRValue val declaredMethod: JIRTypedMethod @@ -639,8 +641,9 @@ data class JIRLambdaExpr( val isNewInvokeSpecial: Boolean get() = lambdaInvokeKind == BsmHandleTag.MethodHandle.NEW_INVOKE_SPECIAL - override val method get() = bsmRef.method + val bootstrapMethod get() = bsmRef.method override val args get() = callSiteArgs + override val type get() = callSiteReturnType override fun accept(visitor: JIRExprVisitor): T { return visitor.visitJIRLambdaExpr(this) @@ -656,8 +659,9 @@ data class JIRDynamicCallExpr( val callSiteArgs: List, ) : JIRCallExpr { - override val method get() = bsmRef.method + val bootstrapMethod get() = bsmRef.method override val args get() = callSiteArgs + override val type get() = callSiteReturnType override fun accept(visitor: JIRExprVisitor): T { return visitor.visitJIRDynamicCallExpr(this) @@ -695,7 +699,7 @@ data class JIRVirtualCallExpr( data class JIRStaticCallExpr( private val methodRef: TypedMethodRef, override val args: List, -) : JIRCallExpr { +) : JIRMethodCallExpr { override val method: JIRTypedMethod get() = methodRef.method diff --git a/core/opentaint-ir/opentaint-ir-core/src/main/kotlin/org/opentaint/ir/impl/cfg/GraphExt.kt b/core/opentaint-ir/opentaint-ir-core/src/main/kotlin/org/opentaint/ir/impl/cfg/GraphExt.kt index e2fe1496a..e2fd908c8 100644 --- a/core/opentaint-ir/opentaint-ir-core/src/main/kotlin/org/opentaint/ir/impl/cfg/GraphExt.kt +++ b/core/opentaint-ir/opentaint-ir-core/src/main/kotlin/org/opentaint/ir/impl/cfg/GraphExt.kt @@ -292,11 +292,7 @@ open class JIRExceptionResolver( } override fun visitJIRLambdaExpr(expr: JIRLambdaExpr): List { - return buildList { - add(runtimeExceptionType) - add(errorType) - addAll(expr.method.exceptions.thisOrThrowable()) - } + return listOf(runtimeExceptionType, errorType) } override fun visitJIRDynamicCallExpr(expr: JIRDynamicCallExpr): List { diff --git a/core/opentaint-ir/opentaint-ir-core/src/test/kotlin/org/opentaint/ir/testing/UnknownClassesTest.kt b/core/opentaint-ir/opentaint-ir-core/src/test/kotlin/org/opentaint/ir/testing/UnknownClassesTest.kt index f5baa291f..c33f6af44 100644 --- a/core/opentaint-ir/opentaint-ir-core/src/test/kotlin/org/opentaint/ir/testing/UnknownClassesTest.kt +++ b/core/opentaint-ir/opentaint-ir-core/src/test/kotlin/org/opentaint/ir/testing/UnknownClassesTest.kt @@ -1,6 +1,7 @@ package org.opentaint.ir.testing import org.opentaint.ir.api.jvm.JIRMethod +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.ext.cfg.callExpr import org.opentaint.ir.api.jvm.ext.cfg.fieldRef import org.opentaint.ir.api.jvm.ext.findClass @@ -80,7 +81,7 @@ class UnknownClassesTest : BaseTest() { val cfg = flowGraph() cfg.instructions.forEach { it.callExpr?.let { - assertNotNull(it.method) + assertNotNull((it as? JIRMethodCallExpr)?.method) } it.fieldRef?.let { assertNotNull(it.field) diff --git a/core/opentaint-ir/opentaint-ir-core/src/test/kotlin/org/opentaint/ir/testing/cfg/InstructionsTest.kt b/core/opentaint-ir/opentaint-ir-core/src/test/kotlin/org/opentaint/ir/testing/cfg/InstructionsTest.kt index dc27a7f06..df3212569 100644 --- a/core/opentaint-ir/opentaint-ir-core/src/test/kotlin/org/opentaint/ir/testing/cfg/InstructionsTest.kt +++ b/core/opentaint-ir/opentaint-ir-core/src/test/kotlin/org/opentaint/ir/testing/cfg/InstructionsTest.kt @@ -43,7 +43,9 @@ class InstructionsTest : BaseInstructionsTest() { val bench = cp.findClass() val use = bench.declaredMethods.first { it.name == "use" } val instructions = method.instList.instructions - val firstUse = instructions.indexOfFirst { it.callExpr?.method?.method == use } + val firstUse = instructions.indexOfFirst { + (it.callExpr as? JIRMethodCallExpr)?.method?.method == use + } val assign = instructions[firstUse + 1] as JIRAssignInst assertEquals("b", (assign.lhv as JIRLocalVar).name) assertEquals("a", (assign.rhv as JIRLocalVar).name) @@ -54,7 +56,9 @@ class InstructionsTest : BaseInstructionsTest() { val clazz = cp.findClass() val method = clazz.declaredMethods.first { it.name == "invoke" } val instructions = method.instList.instructions - val usedArgumentExprs = instructions.filter { it.callExpr?.method?.method?.name == "println" } + val usedArgumentExprs = instructions.filter { + (it.callExpr as? JIRMethodCallExpr)?.method?.method?.name == "println" + } .flatMap { it.callExpr?.args.orEmpty() } val usedArgumentNames = usedArgumentExprs.map { (it as JIRArgument).name } assertEquals(listOf("i", "j", "b", "d"), usedArgumentNames) @@ -248,7 +252,7 @@ class InstructionsTest : BaseInstructionsTest() { val callDoSmth = instList.mapNotNull { it.callExpr }.first { it.toString().contains("doSmth") } - assertEquals("doSmth", callDoSmth.method.method.name) + assertEquals("doSmth", (callDoSmth as JIRMethodCallExpr).method.method.name) } @Test @@ -258,7 +262,7 @@ class InstructionsTest : BaseInstructionsTest() { val callDefaultMethod = instList.mapNotNull { it.callExpr }.first { it.toString().contains("defaultMethod") } - assertEquals("defaultMethod", callDefaultMethod.method.method.name) + assertEquals("defaultMethod", (callDefaultMethod as JIRMethodCallExpr).method.method.name) } @Test diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternMatcher.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternMatcher.kt index 0dc9d267d..f370e51ec 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternMatcher.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternMatcher.kt @@ -15,6 +15,7 @@ import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRInstanceCallExpr import org.opentaint.ir.api.jvm.cfg.JIRInt import org.opentaint.ir.api.jvm.cfg.JIRLocalVar +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRNewExpr import org.opentaint.ir.api.jvm.cfg.JIRReturnInst import org.opentaint.ir.api.jvm.cfg.JIRStringConstant @@ -703,7 +704,7 @@ class SemgrepJavaPatternMatcher( pattern: MethodInvocation, expr: JIRExpr, ): SemgrepMatchingResult { - if (expr !is JIRCallExpr) { + if (expr !is JIRMethodCallExpr) { val exprs = expandLocalVar(position, expr) val variants = exprs.map { match(it.second, pattern, it.first) } return mergeLocalVarVariantMatches(strategy, variants) diff --git a/core/opentaint-utils/opentaint-jvm-util/src/main/kotlin/org/opentaint/jvm/graph/JApplicationGraphImpl.kt b/core/opentaint-utils/opentaint-jvm-util/src/main/kotlin/org/opentaint/jvm/graph/JApplicationGraphImpl.kt index 857cb8a04..e2eb3ec71 100644 --- a/core/opentaint-utils/opentaint-jvm-util/src/main/kotlin/org/opentaint/jvm/graph/JApplicationGraphImpl.kt +++ b/core/opentaint-utils/opentaint-jvm-util/src/main/kotlin/org/opentaint/jvm/graph/JApplicationGraphImpl.kt @@ -3,6 +3,7 @@ package org.opentaint.jvm.graph import org.opentaint.ir.api.jvm.JIRClasspath import org.opentaint.ir.api.jvm.JIRMethod import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.ext.cfg.callExpr import org.opentaint.ir.impl.features.SyncUsagesExtension @@ -36,14 +37,14 @@ open class JApplicationGraphImpl( } override fun callees(node: JIRInst): Sequence { - val callExpr = node.callExpr ?: return emptySequence() + val callExpr = node.callExpr as? JIRMethodCallExpr ?: return emptySequence() return sequenceOf(callExpr.method.method) } override fun callers(method: JIRMethod): Sequence { return usages.findUsages(method).flatMap { it.flowGraph().instructions.asSequence().filter { inst -> - val callExpr = inst.callExpr ?: return@filter false + val callExpr = inst.callExpr as? JIRMethodCallExpr ?: return@filter false callExpr.method.method == method } } diff --git a/core/opentaint-utils/opentaint-jvm-util/src/main/kotlin/org/opentaint/jvm/transformer/JStringConcatTransformer.kt b/core/opentaint-utils/opentaint-jvm-util/src/main/kotlin/org/opentaint/jvm/transformer/JStringConcatTransformer.kt index b59a59ff5..094f6ccf8 100644 --- a/core/opentaint-utils/opentaint-jvm-util/src/main/kotlin/org/opentaint/jvm/transformer/JStringConcatTransformer.kt +++ b/core/opentaint-utils/opentaint-jvm-util/src/main/kotlin/org/opentaint/jvm/transformer/JStringConcatTransformer.kt @@ -41,7 +41,7 @@ object JStringConcatTransformer : JIRInstExtFeature { val stringConcatCalls = list.mapNotNull { inst -> val assignInst = inst as? JIRAssignInst ?: return@mapNotNull null val invokeDynamicExpr = assignInst.rhv as? JIRDynamicCallExpr ?: return@mapNotNull null - if (!methodIsStringConcat(invokeDynamicExpr.method.method)) return@mapNotNull null + if (!methodIsStringConcat(invokeDynamicExpr.bootstrapMethod.method)) return@mapNotNull null assignInst to invokeDynamicExpr } diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/ast/AbstractAstSpanResolver.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/ast/AbstractAstSpanResolver.kt index e710edba3..55b4971c2 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/ast/AbstractAstSpanResolver.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/ast/AbstractAstSpanResolver.kt @@ -15,6 +15,7 @@ import org.opentaint.ir.api.jvm.cfg.JIRCallInst import org.opentaint.ir.api.jvm.cfg.JIRFieldRef import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRInstanceCallExpr +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRNewExpr import org.opentaint.ir.api.jvm.cfg.JIRReturnInst import org.opentaint.ir.api.jvm.cfg.JIRValue @@ -51,7 +52,7 @@ abstract class AbstractAstSpanResolver(protected val traits: JIRSarifTraits) : A } protected fun isConstructorCall(call: JIRCallExpr): Boolean = - call.method.method.isConstructor + (call as? JIRMethodCallExpr)?.method?.method?.isConstructor == true protected fun inferAssignKind(assign: JIRAssignInst): InstructionKind { val l = assign.lhv @@ -170,7 +171,7 @@ abstract class AbstractAstSpanResolver(protected val traits: JIRSarifTraits) : A protected fun JIRInst?.getAssignee(): String? { // fix for initializer calls that are assignments in source - if (this is JIRCallInst && callExpr.method.method.isConstructor && callExpr is JIRInstanceCallExpr) { + if (this is JIRCallInst && isConstructorCall(callExpr) && callExpr is JIRInstanceCallExpr) { return getRawValue((callExpr as JIRInstanceCallExpr).instance) } if (this !is JIRAssignInst) return null diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/ast/JavaAstSpanResolver.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/ast/JavaAstSpanResolver.kt index ca6c3a7f1..cb0b3ff1d 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/ast/JavaAstSpanResolver.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/ast/JavaAstSpanResolver.kt @@ -9,6 +9,7 @@ import org.antlr.v4.runtime.tree.TerminalNode import org.opentaint.dataflow.jvm.util.callee import org.opentaint.ir.api.jvm.JIRMethod import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.ext.cfg.callExpr import org.opentaint.jvm.sast.sarif.IntermediateLocation import org.opentaint.jvm.sast.sarif.JIRSarifTraits @@ -188,7 +189,7 @@ class JavaAstSpanResolver(traits: JIRSarifTraits) : AbstractAstSpanResolver(trai } private fun findMethodCallNode(root: ParseTree, line: Int, inst: JIRInst): ParserRuleContext? { - val call = inst.callExpr ?: return oldFindMethodCallNode(root, line) + val call = inst.callExpr as? JIRMethodCallExpr ?: return oldFindMethodCallNode(root, line) val callee = call.callee.name @@ -235,7 +236,7 @@ class JavaAstSpanResolver(traits: JIRSarifTraits) : AbstractAstSpanResolver(trai } private fun findObjectCreationNode(root: ParseTree, line: Int, inst: JIRInst): ParserRuleContext? { - val callExpr = inst.callExpr ?: return null + val callExpr = inst.callExpr as? JIRMethodCallExpr ?: return null val typeName = callExpr.method.method.enclosingClass.simpleName val creations = collectContexts(root, line) { checkCreatedType(it, typeName) } return adjustForAssignment(creations.maxByOrNull { spanLen(it) }, inst) diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/ast/KotlinAstSpanResolver.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/ast/KotlinAstSpanResolver.kt index 351e81cee..d796e8b02 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/ast/KotlinAstSpanResolver.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/ast/KotlinAstSpanResolver.kt @@ -11,6 +11,7 @@ import org.opentaint.dataflow.jvm.util.callee import org.opentaint.jvm.sast.sarif.JIRSarifTraits import org.opentaint.ir.api.jvm.JIRMethod import org.opentaint.ir.api.jvm.cfg.JIRInst +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.ext.cfg.callExpr import org.opentaint.jvm.sast.sarif.IntermediateLocation import org.opentaint.jvm.sast.sarif.LocationSpan @@ -325,7 +326,7 @@ class KotlinAstSpanResolver(traits: JIRSarifTraits) : AbstractAstSpanResolver(tr } private fun findMethodCallNode(root: ParseTree, line: Int, inst: JIRInst): ParserRuleContext? { - val call = inst.callExpr ?: return oldFindMethodCallNode(root, line) + val call = inst.callExpr as? JIRMethodCallExpr ?: return oldFindMethodCallNode(root, line) val callee = call.callee.name @@ -382,7 +383,7 @@ class KotlinAstSpanResolver(traits: JIRSarifTraits) : AbstractAstSpanResolver(tr } private fun findObjectCreationNode(root: ParseTree, line: Int, inst: JIRInst): ParserRuleContext? { - val callExpr = inst.callExpr ?: return null + val callExpr = inst.callExpr as? JIRMethodCallExpr ?: return null val typeName = callExpr.method.method.enclosingClass.simpleName val creations = collectContexts(root, line) { checkCreatedType(it, typeName) } return adjustForAssignment(creations.maxByOrNull { spanLen(it) }, inst) @@ -561,4 +562,3 @@ class KotlinAstSpanResolver(traits: JIRSarifTraits) : AbstractAstSpanResolver(tr override fun visitChildren(node: RuleNode) = visitChildrenWithLine(node, line) } } - diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/project/JavaPropertiesResolveTransformer.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/project/JavaPropertiesResolveTransformer.kt index ded47a54c..c84c94b76 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/project/JavaPropertiesResolveTransformer.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/project/JavaPropertiesResolveTransformer.kt @@ -11,6 +11,7 @@ import org.opentaint.ir.api.jvm.cfg.JIRImmediate import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRInstList import org.opentaint.ir.api.jvm.cfg.JIRInstanceCallExpr +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRStringConstant import org.opentaint.ir.api.jvm.cfg.JIRValue import org.opentaint.ir.impl.fs.BuildFolderLocation @@ -181,7 +182,7 @@ class JavaPropertiesResolveTransformer( private inline fun traverseCalls( start: JIRInst, instructions: JIRInstList, - body: (JIRInst, JIRCallExpr) -> Unit + body: (JIRInst, JIRMethodCallExpr) -> Unit ): Nothing? { var instIdx = start.location.index while (instIdx >= 0) { @@ -210,7 +211,7 @@ class JavaPropertiesResolveTransformer( ) } - private fun JIRInst.findGetPropertyCall(): JIRCallExpr? { + private fun JIRInst.findGetPropertyCall(): JIRMethodCallExpr? { val call = findCallExpr() ?: return null if (!call.method.method.isGetProperty()) return null return call @@ -225,9 +226,9 @@ class JavaPropertiesResolveTransformer( private fun JIRMethod.isGetResource(): Boolean = name == GET_RESOURCE && enclosingClass.name == CLASS_LOADER - private fun JIRInst.findCallExpr(): JIRCallExpr? = when (this) { - is JIRAssignInst -> rhv as? JIRCallExpr - is JIRCallInst -> callExpr + private fun JIRInst.findCallExpr(): JIRMethodCallExpr? = when (this) { + is JIRAssignInst -> rhv as? JIRMethodCallExpr + is JIRCallInst -> callExpr as? JIRMethodCallExpr else -> null } diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringReactorOperatorsTransformer.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringReactorOperatorsTransformer.kt index 77ea667c9..f3c91c39e 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringReactorOperatorsTransformer.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringReactorOperatorsTransformer.kt @@ -8,12 +8,12 @@ import org.opentaint.ir.api.jvm.JIRType import org.opentaint.ir.api.jvm.PredefinedPrimitives import org.opentaint.ir.api.jvm.cfg.JIRAssignInst import org.opentaint.ir.api.jvm.cfg.JIRBool -import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRClassConstant import org.opentaint.ir.api.jvm.cfg.JIRFieldRef import org.opentaint.ir.api.jvm.cfg.JIRGraph import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRInstList +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRReturnInst import org.opentaint.ir.api.jvm.cfg.JIRStringConstant import org.opentaint.ir.api.jvm.cfg.JIRValue @@ -74,7 +74,7 @@ object SpringReactorOperatorsTransformer : JIRInstExtFeature { private fun findOperatorsSetField( graph: JIRGraph, setInst: JIRInst, - setCall: JIRCallExpr, + setCall: JIRMethodCallExpr, classInitializer: JIRMethod ): Pair? { val (fieldRef, instance, fieldValue) = setCall.args @@ -90,7 +90,7 @@ object SpringReactorOperatorsTransformer : JIRInstExtFeature { ?: return null val fieldUpdaterRef = JIRFieldRef(instance = null, fieldUpdater) - val fieldUpdaterCall = findSingleAssignedValue( + val fieldUpdaterCall = findSingleAssignedValue( classInitializerGraph, clinitReturn, fieldUpdaterRef )?.takeIf { it.method.method.isJavaAtomicRefFieldUpdater() } ?: return null @@ -148,9 +148,9 @@ object SpringReactorOperatorsTransformer : JIRInstExtFeature { return results.singleOrNull() } - private fun findOperatorsSet(inst: JIRInst): JIRCallExpr? { - val call = inst.callExpr - val method = call?.method ?: return null + private fun findOperatorsSet(inst: JIRInst): JIRMethodCallExpr? { + val call = inst.callExpr as? JIRMethodCallExpr ?: return null + val method = call.method if (!method.isStatic) return null if (method.enclosingType.typeName != OPERATORS_CLASS) return null diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/sarif/JIRSarifTraits.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/sarif/JIRSarifTraits.kt index 946f585f1..9df1f9aae 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/sarif/JIRSarifTraits.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/sarif/JIRSarifTraits.kt @@ -16,6 +16,7 @@ import org.opentaint.ir.api.jvm.cfg.JIRFieldRef import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRInstanceCallExpr import org.opentaint.ir.api.jvm.cfg.JIRLocalVar +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRThis import org.opentaint.ir.api.jvm.cfg.JIRValue import org.opentaint.ir.api.jvm.cfg.values @@ -85,7 +86,7 @@ class JIRSarifTraits( printThis(statement) { getReadableInstance(it) } private inline fun printThis(statement: JIRInst, parseStatement: (JIRInst) -> String?): String = - if (statement.callExpr?.callee?.isConstructor == true) { + if ((statement.callExpr as? JIRMethodCallExpr)?.callee?.isConstructor == true) { "the created object" } else { parseStatement(statement) ?: "the calling object" @@ -108,12 +109,12 @@ class JIRSarifTraits( } override fun getCallee(callExpr: CommonCallExpr): JIRMethod { - check(callExpr is JIRCallExpr) + check(callExpr is JIRMethodCallExpr) { "Dynamic call sites do not have a callee method" } return callExpr.callee } override fun getCalleeClassName(callExpr: CommonCallExpr): String { - check(callExpr is JIRCallExpr) + check(callExpr is JIRMethodCallExpr) { "Dynamic call sites do not have a callee method" } return callExpr.callee.enclosingClass.simpleName } diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/sarif/TraceMessageBuilder.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/sarif/TraceMessageBuilder.kt index 18300af11..46b69e9e1 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/sarif/TraceMessageBuilder.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/sarif/TraceMessageBuilder.kt @@ -30,6 +30,7 @@ import org.opentaint.ir.api.jvm.cfg.JIRCallInst import org.opentaint.ir.api.jvm.cfg.JIRGraph import org.opentaint.ir.api.jvm.cfg.JIRInst import org.opentaint.ir.api.jvm.cfg.JIRLocalVar +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRReturnInst import org.opentaint.ir.api.jvm.cfg.JIRThis import org.opentaint.ir.api.jvm.cfg.JIRThrowInst @@ -116,7 +117,7 @@ class TraceMessageBuilder( private val markedVararg = hashMapOf>() private fun JIRInst.getCallVararg(): CommonValue? { - val call = (traits.getCallExpr(this) as JIRCallExpr?) ?: return null + val call = traits.getCallExpr(this) as? JIRMethodCallExpr ?: return null if (!call.isVararg() || call.args.isEmpty()) return null return call.args.last() } @@ -133,7 +134,8 @@ class TraceMessageBuilder( return } - val expr = call.callExpr + val expr = call.callExpr as? JIRMethodCallExpr + ?: return // skip `this` as the first argument as it is not used in lambda's call val captureStart = if (expr.args.isNotEmpty() && expr.args[0] is JIRThis) 1 else 0 val lambdaCapture = expr.args.drop(captureStart).map { param -> @@ -192,7 +194,8 @@ class TraceMessageBuilder( } private fun CommonInst.isLambdaCreation() = - this is JIRCallInst && this.callExpr.method.method is JIRLambdaMethod + this is JIRCallInst && + (this.callExpr as? JIRMethodCallExpr)?.method?.method is JIRLambdaMethod private fun TracePathNode.isLambdaCreation() = this.statement.isLambdaCreation() diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/ast/JavaAstSpanResolverTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/ast/JavaAstSpanResolverTest.kt index 862b4a176..057eac7fe 100644 --- a/core/src/test/kotlin/org/opentaint/jvm/sast/ast/JavaAstSpanResolverTest.kt +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/ast/JavaAstSpanResolverTest.kt @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test import org.opentaint.ir.api.jvm.cfg.JIRArrayAccess import org.opentaint.ir.api.jvm.cfg.JIRAssignInst import org.opentaint.ir.api.jvm.cfg.JIRCallExpr +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRCallInst import org.opentaint.ir.api.jvm.cfg.JIRFieldRef import org.opentaint.ir.api.jvm.cfg.JIRNullConstant @@ -40,7 +41,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val getValueAssign = assignInsts.find { inst -> val rhv = inst.rhv - rhv is JIRCallExpr && rhv.method.method.name == "getValue" + rhv is JIRMethodCallExpr && rhv.method.method.name == "getValue" } checkNotNull(getValueAssign) { "Assignment with getValue() call not found" } @@ -130,7 +131,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(SAMPLE_FQN, "createObject") val constructorCall = callInsts.find { - it.callExpr.method.method.isConstructor + it.callExpr.methodOrNull!!.method.isConstructor } checkNotNull(constructorCall) { "Constructor call not found" } @@ -163,7 +164,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(SAMPLE_FQN, "chainedCall") val toUpperCaseCall = callInsts.find { - it.callExpr?.method?.method?.name == "toUpperCase" + it.callExpr?.methodOrNull?.method?.name == "toUpperCase" } checkNotNull(toUpperCaseCall) { "toUpperCase() call not found in chain" } @@ -202,7 +203,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(STATIC_SAMPLE_FQN, "staticMethod") val toLowerCaseCall = callInsts.find { - it.callExpr?.method?.method?.name == "toLowerCase" + it.callExpr?.methodOrNull?.method?.name == "toLowerCase" } checkNotNull(toLowerCaseCall) { "toLowerCase() call not found" } @@ -246,7 +247,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(SAMPLE_FQN, "simpleMethodCall") val toUpperCaseCall = callInsts.find { - it.callExpr?.method?.method?.name == "toUpperCase" + it.callExpr?.methodOrNull?.method?.name == "toUpperCase" } checkNotNull(toUpperCaseCall) { "toUpperCase() call not found" } @@ -382,11 +383,11 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val instructions = method.instList val expectedSpans = mapOf( - "entry" to findInstruction(instructions) { it.rhv is JIRCallExpr && (it.rhv as JIRCallExpr).method.method.name == "trim" }, - "local" to findInstruction(instructions) { it.rhv is JIRCallExpr && (it.rhv as JIRCallExpr).method.method.name == "trim" }, + "entry" to findInstruction(instructions) { it.rhv is JIRMethodCallExpr && (it.rhv as JIRMethodCallExpr).method.method.name == "trim" }, + "local" to findInstruction(instructions) { it.rhv is JIRMethodCallExpr && (it.rhv as JIRMethodCallExpr).method.method.name == "trim" }, "fieldWrite" to findInstruction(instructions) { it.lhv is JIRFieldRef }, "fieldRead" to findInstruction(instructions) { it.rhv is JIRFieldRef }, - "call" to findInstruction(instructions) { it.rhv is JIRCallExpr && (it.rhv as JIRCallExpr).method.method.name == "toUpperCase" }, + "call" to findInstruction(instructions) { it.rhv is JIRMethodCallExpr && (it.rhv as JIRMethodCallExpr).method.method.name == "toUpperCase" }, "return" to findInstruction(instructions) { true }, "exit" to findInstruction(instructions) { true } ) @@ -540,7 +541,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(SAMPLE_FQN, "methodCallNoArgs") val noArgsCall = callInsts.find { - it.callExpr.method.method.name == "noArgsHelper" + it.callExpr.methodOrNull!!.method.name == "noArgsHelper" } checkNotNull(noArgsCall) { "noArgsHelper() call not found" } @@ -558,7 +559,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(SAMPLE_FQN, "methodCallOneArg") val oneArgCall = callInsts.find { - it.callExpr.method.method.name == "oneArgHelper" + it.callExpr.methodOrNull!!.method.name == "oneArgHelper" } checkNotNull(oneArgCall) { "oneArgHelper() call not found" } @@ -576,7 +577,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(SAMPLE_FQN, "methodCallTwoArgs") val twoArgsCall = callInsts.find { - it.callExpr.method.method.name == "twoArgsHelper" + it.callExpr.methodOrNull!!.method.name == "twoArgsHelper" } checkNotNull(twoArgsCall) { "twoArgsHelper() call not found" } @@ -594,7 +595,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(SAMPLE_FQN, "methodCallThreeArgs") val threeArgsCall = callInsts.find { - it.callExpr.method.method.name == "threeArgsHelper" + it.callExpr.methodOrNull!!.method.name == "threeArgsHelper" } checkNotNull(threeArgsCall) { "threeArgsHelper() call not found" } @@ -612,7 +613,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(SAMPLE_FQN, "methodCallVarargs") val varargsCall = callInsts.find { - it.callExpr.method.method.name == "varargsHelper" + it.callExpr.methodOrNull!!.method.name == "varargsHelper" } checkNotNull(varargsCall) { "varargsHelper() call not found" } @@ -631,7 +632,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val sourcePath = getSourcePath(SAMPLE_FQN) val callInsts = getInstructionsOfType(SAMPLE_FQN, "localVarCallNoArgs") - val call = callInsts.find { it.callExpr?.method?.method?.name == "length" } + val call = callInsts.find { it.callExpr?.methodOrNull?.method?.name == "length" } checkNotNull(call) { "length() call not found" } val location = createIntermediateLocation(call) @@ -646,7 +647,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val sourcePath = getSourcePath(SAMPLE_FQN) val callInsts = getInstructionsOfType(SAMPLE_FQN, "localVarCallOneArg") - val call = callInsts.find { it.callExpr?.method?.method?.name == "charAt" } + val call = callInsts.find { it.callExpr?.methodOrNull?.method?.name == "charAt" } checkNotNull(call) { "charAt() call not found" } val location = createIntermediateLocation(call) @@ -661,7 +662,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val sourcePath = getSourcePath(SAMPLE_FQN) val callInsts = getInstructionsOfType(SAMPLE_FQN, "localVarCallTwoArgs") - val call = callInsts.find { it.callExpr?.method?.method?.name == "substring" } + val call = callInsts.find { it.callExpr?.methodOrNull?.method?.name == "substring" } checkNotNull(call) { "substring() call not found" } val location = createIntermediateLocation(call) @@ -676,7 +677,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val sourcePath = getSourcePath(SAMPLE_FQN) val callInsts = getInstructionsOfType(SAMPLE_FQN, "localVarCallThreeArgs") - val call = callInsts.find { it.callExpr?.method?.method?.name == "replace" } + val call = callInsts.find { it.callExpr?.methodOrNull?.method?.name == "replace" } checkNotNull(call) { "replace() call not found" } val location = createIntermediateLocation(call) @@ -693,7 +694,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val sourcePath = getSourcePath(SAMPLE_FQN) val callInsts = getInstructionsOfType(SAMPLE_FQN, "chainedCallNoArgs") - val call = callInsts.find { it.callExpr?.method?.method?.name == "length" } + val call = callInsts.find { it.callExpr?.methodOrNull?.method?.name == "length" } checkNotNull(call) { "length() call not found" } val location = createIntermediateLocation(call) @@ -708,7 +709,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val sourcePath = getSourcePath(SAMPLE_FQN) val callInsts = getInstructionsOfType(SAMPLE_FQN, "chainedCallOneArg") - val call = callInsts.find { it.callExpr?.method?.method?.name == "charAt" } + val call = callInsts.find { it.callExpr?.methodOrNull?.method?.name == "charAt" } checkNotNull(call) { "charAt() call not found" } val location = createIntermediateLocation(call) @@ -723,7 +724,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val sourcePath = getSourcePath(SAMPLE_FQN) val callInsts = getInstructionsOfType(SAMPLE_FQN, "chainedCallTwoArgs") - val call = callInsts.find { it.callExpr?.method?.method?.name == "substring" } + val call = callInsts.find { it.callExpr?.methodOrNull?.method?.name == "substring" } checkNotNull(call) { "substring() call not found" } val location = createIntermediateLocation(call) @@ -738,7 +739,7 @@ class JavaAstSpanResolverTest : BasicTestUtils() { val sourcePath = getSourcePath(SAMPLE_FQN) val callInsts = getInstructionsOfType(SAMPLE_FQN, "chainedCallThreeArgs") - val call = callInsts.find { it.callExpr?.method?.method?.name == "replace" } + val call = callInsts.find { it.callExpr?.methodOrNull?.method?.name == "replace" } checkNotNull(call) { "replace() call not found" } val location = createIntermediateLocation(call) @@ -749,3 +750,5 @@ class JavaAstSpanResolverTest : BasicTestUtils() { } } +private val JIRCallExpr.methodOrNull + get() = (this as? JIRMethodCallExpr)?.method diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/ast/KotlinAstSpanResolverTest.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/ast/KotlinAstSpanResolverTest.kt index 7014bddb7..ac86d93fd 100644 --- a/core/src/test/kotlin/org/opentaint/jvm/sast/ast/KotlinAstSpanResolverTest.kt +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/ast/KotlinAstSpanResolverTest.kt @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test import org.opentaint.ir.api.jvm.cfg.JIRArrayAccess import org.opentaint.ir.api.jvm.cfg.JIRAssignInst import org.opentaint.ir.api.jvm.cfg.JIRCallExpr +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRCallInst import org.opentaint.ir.api.jvm.cfg.JIRFieldRef import org.opentaint.ir.api.jvm.cfg.JIRInst @@ -61,7 +62,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val getValueAssign = assignInsts.find { inst -> val rhv = inst.rhv - rhv is JIRCallExpr && rhv.method.method.name == "getFieldValue" + rhv is JIRMethodCallExpr && rhv.method.method.name == "getFieldValue" } checkNotNull(getValueAssign) { "Assignment with getFieldValue() call not found" } @@ -151,7 +152,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(SAMPLE_FQN, "createObject") val constructorCall = callInsts.find { - it.callExpr.method.method.isConstructor + it.callExpr.methodOrNull!!.method.isConstructor } checkNotNull(constructorCall) { "Constructor call not found" } @@ -323,8 +324,8 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val instructions = method.instList val expectedSpans = mapOf( - "entry" to findInstruction(instructions) { it.rhv is JIRCallExpr && (it.rhv as JIRCallExpr).method.method.name == "trim" }, - "local" to findInstruction(instructions) { it.rhv is JIRCallExpr && (it.rhv as JIRCallExpr).method.method.name == "trim" }, + "entry" to findInstruction(instructions) { it.rhv is JIRMethodCallExpr && (it.rhv as JIRMethodCallExpr).method.method.name == "trim" }, + "local" to findInstruction(instructions) { it.rhv is JIRMethodCallExpr && (it.rhv as JIRMethodCallExpr).method.method.name == "trim" }, "fieldWrite" to findInstruction(instructions) { it.lhv is JIRFieldRef }, "fieldRead" to findInstruction(instructions) { it.rhv is JIRFieldRef }, "return" to findInstruction(instructions) { true }, @@ -550,7 +551,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val assignInsts = method.instList.filterIsInstance() val extensionCall = assignInsts.find { - it.callExpr?.method?.method?.name == "myExtension" + it.callExpr?.methodOrNull?.method?.name == "myExtension" } checkNotNull(extensionCall) { "Extension function call not found" } @@ -584,7 +585,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val assignInsts = method.instList.filterIsInstance() val plusCall = assignInsts.find { - it.callExpr?.method?.method?.name == "plus" + it.callExpr?.methodOrNull?.method?.name == "plus" } checkNotNull(plusCall) { "Plus operator call not found" } @@ -722,7 +723,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val callInsts = getInstructionsOfType(DATA_CLASS_WITH_INIT_FQN, "") val printlnCall = callInsts.find { inst -> - inst.callExpr.method.method.name == "println" + inst.callExpr.methodOrNull!!.method.name == "println" } checkNotNull(printlnCall) { "println call in init block not found" } @@ -772,7 +773,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val assignInsts = method.instList.filterIsInstance() val suspendCall = assignInsts.find { - it.callExpr?.method?.method?.name == "suspendFunction" + it.callExpr?.methodOrNull?.method?.name == "suspendFunction" } checkNotNull(suspendCall) { "Suspend function call not found" } @@ -894,7 +895,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val assignInsts = method.instList.filterIsInstance() val uppercaseCall = assignInsts.find { inst -> - inst.callExpr?.method?.method?.name == "toUpperCase" + inst.callExpr?.methodOrNull?.method?.name == "toUpperCase" } checkNotNull(uppercaseCall) { "toUpperCase() call not found in suspend function body" } @@ -910,7 +911,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val assignInsts = method.instList.filterIsInstance() val valueOfCall = assignInsts.find { inst -> - inst.callExpr?.method?.method?.name == "valueOf" + inst.callExpr?.methodOrNull?.method?.name == "valueOf" } checkNotNull(valueOfCall) { "valueOf() call from inlined body not found" } @@ -962,7 +963,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val assignInsts = method.instList.filterIsInstance() val uppercaseCall = assignInsts.find { inst -> - inst.callExpr?.method?.method?.name == "toUpperCase" + inst.callExpr?.methodOrNull?.method?.name == "toUpperCase" } checkNotNull(uppercaseCall) { "uppercase() call inside for loop not found" } @@ -978,7 +979,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val assignInsts = method.instList.filterIsInstance() val getCall = assignInsts.find { inst -> - inst.callExpr?.method?.method?.name == "get" + inst.callExpr?.methodOrNull?.method?.name == "get" } checkNotNull(getCall) { "get() call inside while loop not found" } @@ -1009,10 +1010,10 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val expectedSpans = mutableMapOf() expectedSpans["complexLoopCall"] = findInstruction(instructions) { - it.callExpr?.method?.method?.name == "toUpperCase" + it.callExpr?.methodOrNull?.method?.name == "toUpperCase" } expectedSpans["complexWhileGet"] = findInstruction(instructions) { - it.callExpr?.method?.method?.name == "get" + it.callExpr?.methodOrNull?.method?.name == "get" } expectedSpans["complexReturn"] = findInstruction(instructions) { true } expectedSpans["complexMethodExit"] = findInstruction(instructions) { true } @@ -1054,7 +1055,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { val assignInsts = method.instList.filterIsInstance() val processCall = assignInsts.find { inst -> - inst.callExpr?.method?.method?.name == "process" + inst.callExpr?.methodOrNull?.method?.name == "process" } checkNotNull(processCall) { "process() call not found in expression body" } @@ -1197,7 +1198,7 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { checkNotNull(defaultCtor) { "Default constructor not found" } val primaryCtorCall = findInstruction(defaultCtor.instList) { - it.callExpr.method.method.isConstructor + it.callExpr.methodOrNull!!.method.isConstructor } checkNotNull(primaryCtorCall) { "Primary ctor call not found" } @@ -1248,3 +1249,6 @@ class KotlinAstSpanResolverTest : BasicTestUtils() { private fun getAnnotatedSourcePath() = sourcesDir.resolve("test/samples/KotlinAnnotatedSample.kt") } + +private val JIRCallExpr.methodOrNull + get() = (this as? JIRMethodCallExpr)?.method From ea82a3d28db08fde6943121d1ebf4654ee646888 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Wed, 5 Aug 2026 16:52:56 +0200 Subject: [PATCH 4/4] fix(ci): test dataflow records on Java 17 --- .github/workflows/ci-dataflow.yaml | 11 +++++--- .../samples/build.gradle.kts | 27 ++++++++++++++++--- .../jvm/ap/ifds/alias/AliasSampleTest.kt | 6 +++++ .../ir/approximations/ApproximationsTest.kt | 3 ++- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-dataflow.yaml b/.github/workflows/ci-dataflow.yaml index 82ba9fbf8..806267202 100644 --- a/.github/workflows/ci-dataflow.yaml +++ b/.github/workflows/ci-dataflow.yaml @@ -28,17 +28,22 @@ concurrency: jobs: check: + name: Run tests on JDK ${{ matrix.jdk }} runs-on: ubuntu-latest container: gitlab/gitlab-runner-helper:ubuntu-x86_64-latest + strategy: + fail-fast: false + matrix: + jdk: [ 11, 17 ] permissions: contents: read steps: - uses: actions/checkout@v4 - - name: Set up JDK 11 + - name: Set up JDK ${{ matrix.jdk }} uses: actions/setup-java@v4 with: - java-version: '11' + java-version: ${{ matrix.jdk }} distribution: 'temurin' - name: Install Go-ir dependencies @@ -63,6 +68,6 @@ jobs: if: (!cancelled()) uses: actions/upload-artifact@v4 with: - name: gradle-reports-ci-dataflow + name: gradle-reports-ci-dataflow-jdk${{ matrix.jdk }} path: '**/build/reports/' retention-days: 1 diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts index 1d04df037..0ac5d9da2 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/samples/build.gradle.kts @@ -2,17 +2,36 @@ plugins { java } +val recordSamplePath = "sample/alias/RecordAliasSample.java" +val supportsJava17 = JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17) + +sourceSets.main { + java.exclude(recordSamplePath) +} + tasks { withType { - // Records provide a real unresolved invokedynamic call site for the - // alias-analysis samples. Existing samples remain source-compatible. - sourceCompatibility = JavaVersion.VERSION_17.toString() - targetCompatibility = JavaVersion.VERSION_17.toString() + sourceCompatibility = JavaVersion.VERSION_1_8.toString() + targetCompatibility = JavaVersion.VERSION_1_8.toString() options.compilerArgs.add("-g") } } +val java17 = sourceSets.create("java17") { + java.setSrcDirs(listOf("src/main/java")) + java.include(recordSamplePath) +} + +tasks.named(java17.compileJavaTaskName) { + enabled = supportsJava17 + sourceCompatibility = JavaVersion.VERSION_17.toString() + targetCompatibility = JavaVersion.VERSION_17.toString() +} + tasks.jar { + if (supportsJava17) { + from(java17.output) + } from(sourceSets.main.get().allSource) { include("**/*.java") } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt index 64f9de67f..4047accf9 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/AliasSampleTest.kt @@ -2,6 +2,7 @@ package org.opentaint.dataflow.jvm.ap.ifds.alias import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.TestInstance import org.opentaint.dataflow.ap.ifds.AccessPathBase import org.opentaint.dataflow.ap.ifds.AccessPathBase.Companion.Argument @@ -584,6 +585,8 @@ class AliasSampleTest : BasicTestUtils() { @Test fun `record invokedynamic is not analyzed as a call to its bootstrap method`() { + assumeTrue(javaFeatureVersion() >= 17, "Record fixture requires Java 17") + val method = findMethod(RECORD_SAMPLE, "recordHashCodeInlined") val recordHashCode = cp.findClassOrNull("$RECORD_SAMPLE\$Payload") ?.declaredMethods @@ -647,6 +650,9 @@ class AliasSampleTest : BasicTestUtils() { private fun interProcParams(depth: Int) = JIRLocalAliasAnalysis.Params(useAliasAnalysis = true, aliasAnalysisInterProcCallDepth = depth) + private fun javaFeatureVersion(): Int = + System.getProperty("java.specification.version").removePrefix("1.").substringBefore('.').toInt() + private fun JIRMethod.findSinkCall(sinkName: String): JIRCallInst = instList.filterIsInstance().first { (it.callExpr as? JIRMethodCallExpr)?.method?.name == sinkName diff --git a/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/ApproximationsTest.kt b/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/ApproximationsTest.kt index 82252400e..872b9b5ae 100644 --- a/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/ApproximationsTest.kt +++ b/core/opentaint-ir/opentaint-ir-approximations/src/test/kotlin/org/opentaint/ir/approximations/ApproximationsTest.kt @@ -4,6 +4,7 @@ import org.opentaint.ir.api.jvm.JavaVersion import org.opentaint.ir.api.jvm.cfg.JIRAssignInst import org.opentaint.ir.api.jvm.cfg.JIRCallInst import org.opentaint.ir.api.jvm.cfg.JIRFieldRef +import org.opentaint.ir.api.jvm.cfg.JIRMethodCallExpr import org.opentaint.ir.api.jvm.cfg.JIRRawAssignInst import org.opentaint.ir.api.jvm.cfg.JIRRawCallInst import org.opentaint.ir.api.jvm.cfg.JIRRawFieldRef @@ -151,7 +152,7 @@ open class ApproximationsTest : BaseTest() { val location = inst.location assertTrue(location.method === method) - val callExpr = inst.callExpr + val callExpr = inst.callExpr as? JIRMethodCallExpr ?: return@forEach types += callExpr.type.typeName types += callExpr.method.returnType.typeName types += callExpr.method.enclosingType.typeName