Skip to content

Commit b9f4856

Browse files
authored
Merge pull request #10876 from smowton/smowton/feature/kotlin-default-method-auto-mad
Java models-as-data: infer Kotlin $default models from that of its parent function
2 parents a4258ea + 7a0bded commit b9f4856

File tree

9 files changed

+282
-12
lines changed

9 files changed

+282
-12
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class ConstructorWithDefaults(x: Int, y: Int = 1) { }
2+
3+
fun topLevelWithDefaults(x: Int, y: Int = 1) = 0
4+
fun String.extensionWithDefaults(x: Int, y: Int = 1) = 0
5+
6+
class LibClass {
7+
8+
fun memberWithDefaults(x: Int, y: Int = 1) = 0
9+
fun String.extensionMemberWithDefaults(x: Int, y: Int = 1) = 0
10+
11+
fun multiParameterTest(x: Int, y: Int, z: Int, w: Int = 0) = 0
12+
fun Int.multiParameterExtensionTest(x: Int, y: Int, w: Int = 0) = 0
13+
14+
}
15+
16+
class SomeToken {}
17+
18+
fun topLevelArgSource(st: SomeToken, x: Int = 0) {}
19+
fun String.extensionArgSource(st: SomeToken, x: Int = 0) {}
20+
21+
class SourceClass {
22+
23+
fun memberArgSource(st: SomeToken, x: Int = 0) {}
24+
25+
}
26+
27+
fun topLevelSink(x: Int, y: Int = 1) {}
28+
fun String.extensionSink(x: Int, y: Int = 1) {}
29+
30+
class SinkClass(x: Int, y: Int = 1) {
31+
32+
fun memberSink(x: Int, y: Int = 1) {}
33+
fun String.extensionMemberSink(x: Int, y: Int = 1) {}
34+
35+
}

java/ql/integration-tests/posix-only/kotlin/default-parameter-mad-flow/test.expected

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from create_database_utils import *
2+
import subprocess
3+
4+
subprocess.check_call(["kotlinc", "lib.kt", "-d", "lib"])
5+
run_codeql_database_create(["kotlinc user.kt -cp lib"], lang="java")
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import java
2+
import semmle.code.java.dataflow.TaintTracking
3+
import TestUtilities.InlineExpectationsTest
4+
private import semmle.code.java.dataflow.ExternalFlow
5+
6+
private class Models extends SummaryModelCsv {
7+
override predicate row(string row) {
8+
row =
9+
[
10+
";ConstructorWithDefaults;true;ConstructorWithDefaults;(int,int);;Argument[0];Argument[-1];taint;manual",
11+
";LibKt;true;topLevelWithDefaults;(int,int);;Argument[0];ReturnValue;value;manual",
12+
";LibKt;true;extensionWithDefaults;(String,int,int);;Argument[1];ReturnValue;value;manual",
13+
";LibClass;true;memberWithDefaults;(int,int);;Argument[0];ReturnValue;value;manual",
14+
";LibClass;true;extensionMemberWithDefaults;(String,int,int);;Argument[1];ReturnValue;value;manual",
15+
";LibClass;true;multiParameterTest;(int,int,int,int);;Argument[0..1];ReturnValue;value;manual",
16+
";LibClass;true;multiParameterExtensionTest;(int,int,int,int);;Argument[0, 1];ReturnValue;value;manual",
17+
]
18+
}
19+
}
20+
21+
private class SourceModels extends SourceModelCsv {
22+
override predicate row(string row) {
23+
row =
24+
[
25+
";LibKt;true;topLevelArgSource;(SomeToken,int);;Argument[0];kotlinMadFlowTest;manual",
26+
";LibKt;true;extensionArgSource;(String,SomeToken,int);;Argument[1];kotlinMadFlowTest;manual",
27+
";SourceClass;true;memberArgSource;(SomeToken,int);;Argument[0];kotlinMadFlowTest;manual"
28+
]
29+
}
30+
}
31+
32+
private class SinkModels extends SinkModelCsv {
33+
override predicate row(string row) {
34+
row =
35+
[
36+
";SinkClass;true;SinkClass;(int,int);;Argument[0];kotlinMadFlowTest;manual",
37+
";LibKt;true;topLevelSink;(int,int);;Argument[0];kotlinMadFlowTest;manual",
38+
";LibKt;true;extensionSink;(String,int,int);;Argument[1];kotlinMadFlowTest;manual",
39+
";SinkClass;true;memberSink;(int,int);;Argument[0];kotlinMadFlowTest;manual",
40+
";SinkClass;true;extensionMemberSink;(String,int,int);;Argument[1];kotlinMadFlowTest;manual"
41+
]
42+
}
43+
}
44+
45+
class Config extends TaintTracking::Configuration {
46+
Config() { this = "Config" }
47+
48+
override predicate isSource(DataFlow::Node n) {
49+
n.asExpr().(MethodAccess).getCallee().getName() = "source"
50+
or
51+
sourceNode(n, "kotlinMadFlowTest")
52+
}
53+
54+
override predicate isSink(DataFlow::Node n) {
55+
n.asExpr().(Argument).getCall().getCallee().getName() = "sink"
56+
or
57+
sinkNode(n, "kotlinMadFlowTest")
58+
}
59+
}
60+
61+
class InlineFlowTest extends InlineExpectationsTest {
62+
InlineFlowTest() { this = "HasFlowTest" }
63+
64+
override string getARelevantTag() { result = "flow" }
65+
66+
override predicate hasActualResult(Location location, string element, string tag, string value) {
67+
tag = "flow" and
68+
exists(DataFlow::Node src, DataFlow::Node sink, Config c | c.hasFlow(src, sink) |
69+
sink.getLocation() = location and
70+
element = sink.toString() and
71+
value = ""
72+
)
73+
}
74+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
fun source() = 1
2+
3+
fun sink(x: Any) { }
4+
5+
fun test(c: LibClass, sourcec: SourceClass, sinkc: SinkClass) {
6+
7+
sink(ConstructorWithDefaults(source(), 0)) // $ flow
8+
sink(ConstructorWithDefaults(source())) // $ flow
9+
10+
sink(topLevelWithDefaults(source(), 0)) // $ flow
11+
sink(topLevelWithDefaults(source())) // $ flow
12+
13+
sink("Hello world".extensionWithDefaults(source(), 0)) // $ flow
14+
sink("Hello world".extensionWithDefaults(source())) // $ flow
15+
16+
sink(c.memberWithDefaults(source(), 0)) // $ flow
17+
sink(c.memberWithDefaults(source())) // $ flow
18+
19+
sink(c.multiParameterTest(source(), 0, 0)) // $ flow
20+
sink(c.multiParameterTest(0, source(), 0)) // $ flow
21+
sink(c.multiParameterTest(0, 0, source()))
22+
23+
with(c) {
24+
sink("Hello world".extensionMemberWithDefaults(source(), 0)) // $ flow
25+
sink("Hello world".extensionMemberWithDefaults(source())) // $ flow
26+
}
27+
28+
with(c) {
29+
sink(source().multiParameterExtensionTest(0, 0)) // $ flow
30+
sink(0.multiParameterExtensionTest(source(), 0)) // $ flow
31+
sink(0.multiParameterExtensionTest(0, source()))
32+
}
33+
34+
run {
35+
val st = SomeToken()
36+
topLevelArgSource(st)
37+
sink(st) // $ flow
38+
}
39+
40+
run {
41+
val st = SomeToken()
42+
"Hello world".extensionArgSource(st)
43+
sink(st) // $ flow
44+
}
45+
46+
run {
47+
val st = SomeToken()
48+
sourcec.memberArgSource(st)
49+
sink(st) // $ flow
50+
}
51+
52+
SinkClass(source()) // $ flow
53+
topLevelSink(source()) // $ flow
54+
"Hello world".extensionSink(source()) // $ flow
55+
sinkc.memberSink(source()) // $ flow
56+
with(sinkc) {
57+
"Hello world".extensionMemberSink(source()) // $ flow
58+
}
59+
60+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
libraryPathDependencies:
22
- codeql-java
3+
- codeql/java-tests

java/ql/lib/semmle/code/java/Member.qll

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,9 @@ class Callable extends StmtParent, Member, @callable {
309309
*/
310310
Callable getKotlinParameterDefaultsProxy() {
311311
this.getDeclaringType() = result.getDeclaringType() and
312-
exists(int proxyNParams, int extraLeadingParams, RefType lastParamType |
312+
exists(
313+
int proxyNParams, int extraLeadingParams, int regularParamsStartIdx, RefType lastParamType
314+
|
313315
proxyNParams = result.getNumberOfParameters() and
314316
extraLeadingParams = (proxyNParams - this.getNumberOfParameters()) - 2 and
315317
extraLeadingParams >= 0 and
@@ -319,16 +321,28 @@ class Callable extends StmtParent, Member, @callable {
319321
this instanceof Constructor and
320322
result instanceof Constructor and
321323
extraLeadingParams = 0 and
324+
regularParamsStartIdx = 0 and
322325
lastParamType.hasQualifiedName("kotlin.jvm.internal", "DefaultConstructorMarker")
323326
or
324327
this instanceof Method and
325328
result instanceof Method and
326329
this.getName() + "$default" = result.getName() and
327-
extraLeadingParams <= 2 and
330+
extraLeadingParams <= 1 and
331+
(
332+
if ktExtensionFunctions(this, _, _)
333+
then
334+
// Both extension receivers are expected to occur at arg0, with any
335+
// dispatch receiver inserted afterwards in the $default proxy's parameter list.
336+
// Check the extension receiver matches here, and note regular args
337+
// are bumped one position to the right.
338+
regularParamsStartIdx = extraLeadingParams + 1 and
339+
this.getParameterType(0).getErasure() = eraseRaw(result.getParameterType(0))
340+
else regularParamsStartIdx = extraLeadingParams
341+
) and
328342
lastParamType instanceof TypeObject
329343
)
330344
|
331-
forall(int paramIdx | paramIdx in [extraLeadingParams .. proxyNParams - 3] |
345+
forall(int paramIdx | paramIdx in [regularParamsStartIdx .. proxyNParams - 3] |
332346
this.getParameterType(paramIdx - extraLeadingParams).getErasure() =
333347
eraseRaw(result.getParameterType(paramIdx))
334348
)

java/ql/lib/semmle/code/java/dataflow/internal/FlowSummaryImplSpecific.qll

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ private import FlowSummaryImpl::Private
1010
private import FlowSummaryImpl::Public
1111
private import semmle.code.java.dataflow.ExternalFlow
1212
private import semmle.code.java.dataflow.FlowSummary as FlowSummary
13+
private import semmle.code.java.dataflow.internal.AccessPathSyntax as AccessPathSyntax
1314

1415
class SummarizedCallableBase = FlowSummary::SummarizedCallableBase;
1516

@@ -65,6 +66,66 @@ private boolean isGenerated(string provenance) {
6566
provenance != "generated" and result = false
6667
}
6768

69+
private predicate relatedArgSpec(Callable c, string spec) {
70+
exists(
71+
string namespace, string type, boolean subtypes, string name, string signature, string ext
72+
|
73+
summaryModel(namespace, type, subtypes, name, signature, ext, spec, _, _, _) or
74+
summaryModel(namespace, type, subtypes, name, signature, ext, _, spec, _, _) or
75+
sourceModel(namespace, type, subtypes, name, signature, ext, spec, _, _) or
76+
sinkModel(namespace, type, subtypes, name, signature, ext, spec, _, _)
77+
|
78+
c = interpretElement(namespace, type, subtypes, name, signature, ext)
79+
)
80+
}
81+
82+
/**
83+
* Holds if `defaultsCallable` is a Kotlin default-parameter proxy for `originalCallable`, and
84+
* `originalCallable` has a model, and `defaultsArgSpec` is `originalArgSpec` adjusted to account
85+
* for the additional dispatch receiver parameter that occurs in the default-parameter proxy's argument
86+
* list. When no adjustment is required (e.g. for constructors, or non-argument-based specs), `defaultArgsSpec`
87+
* equals `originalArgSpec`.
88+
*
89+
* Note in the case where `originalArgSpec` uses an integer range, like `Argument[1..3]...`, this will produce multiple
90+
* results for `defaultsArgSpec`, like `{Argument[2]..., Argument[3]..., Argument[4]...}`.
91+
*/
92+
private predicate correspondingKotlinParameterDefaultsArgSpec(
93+
Callable originalCallable, Callable defaultsCallable, string originalArgSpec,
94+
string defaultsArgSpec
95+
) {
96+
relatedArgSpec(originalCallable, originalArgSpec) and
97+
defaultsCallable = originalCallable.getKotlinParameterDefaultsProxy() and
98+
(
99+
originalCallable instanceof Constructor and originalArgSpec = defaultsArgSpec
100+
or
101+
originalCallable instanceof Method and
102+
exists(string regex |
103+
// Note I use a regex and not AccessPathToken because this feeds summaryElement et al,
104+
// which would introduce mutual recursion with the definition of AccessPathToken.
105+
regex = "Argument\\[([0-9,\\. ]+)\\](.*)" and
106+
(
107+
exists(string oldArgNumber, string rest, int paramOffset |
108+
oldArgNumber = originalArgSpec.regexpCapture(regex, 1) and
109+
rest = originalArgSpec.regexpCapture(regex, 2) and
110+
paramOffset =
111+
defaultsCallable.getNumberOfParameters() -
112+
(originalCallable.getNumberOfParameters() + 2) and
113+
exists(int oldArgParsed |
114+
oldArgParsed = AccessPathSyntax::AccessPath::parseInt(oldArgNumber.splitAt(",").trim())
115+
|
116+
if ktExtensionFunctions(originalCallable, _, _) and oldArgParsed = 0
117+
then defaultsArgSpec = "Argument[0]"
118+
else defaultsArgSpec = "Argument[" + (oldArgParsed + paramOffset) + "]" + rest
119+
)
120+
)
121+
or
122+
not originalArgSpec.regexpMatch(regex) and
123+
defaultsArgSpec = originalArgSpec
124+
)
125+
)
126+
)
127+
}
128+
68129
/**
69130
* Holds if an external flow summary exists for `c` with input specification
70131
* `input`, output specification `output`, kind `kind`, and a flag `generated`
@@ -75,11 +136,19 @@ predicate summaryElement(
75136
) {
76137
exists(
77138
string namespace, string type, boolean subtypes, string name, string signature, string ext,
78-
string provenance
139+
string provenance, string originalInput, string originalOutput, Callable baseCallable
79140
|
80-
summaryModel(namespace, type, subtypes, name, signature, ext, input, output, kind, provenance) and
141+
summaryModel(namespace, type, subtypes, name, signature, ext, originalInput, originalOutput,
142+
kind, provenance) and
81143
generated = isGenerated(provenance) and
82-
c.asCallable() = interpretElement(namespace, type, subtypes, name, signature, ext)
144+
baseCallable = interpretElement(namespace, type, subtypes, name, signature, ext) and
145+
(
146+
c.asCallable() = baseCallable and input = originalInput and output = originalOutput
147+
or
148+
correspondingKotlinParameterDefaultsArgSpec(baseCallable, c.asCallable(), originalInput, input) and
149+
correspondingKotlinParameterDefaultsArgSpec(baseCallable, c.asCallable(), originalOutput,
150+
output)
151+
)
83152
)
84153
}
85154

@@ -149,11 +218,16 @@ class SourceOrSinkElement = Top;
149218
predicate sourceElement(SourceOrSinkElement e, string output, string kind, boolean generated) {
150219
exists(
151220
string namespace, string type, boolean subtypes, string name, string signature, string ext,
152-
string provenance
221+
string provenance, SourceOrSinkElement baseSource, string originalOutput
153222
|
154-
sourceModel(namespace, type, subtypes, name, signature, ext, output, kind, provenance) and
223+
sourceModel(namespace, type, subtypes, name, signature, ext, originalOutput, kind, provenance) and
155224
generated = isGenerated(provenance) and
156-
e = interpretElement(namespace, type, subtypes, name, signature, ext)
225+
baseSource = interpretElement(namespace, type, subtypes, name, signature, ext) and
226+
(
227+
e = baseSource and output = originalOutput
228+
or
229+
correspondingKotlinParameterDefaultsArgSpec(baseSource, e, originalOutput, output)
230+
)
157231
)
158232
}
159233

@@ -165,11 +239,16 @@ predicate sourceElement(SourceOrSinkElement e, string output, string kind, boole
165239
predicate sinkElement(SourceOrSinkElement e, string input, string kind, boolean generated) {
166240
exists(
167241
string namespace, string type, boolean subtypes, string name, string signature, string ext,
168-
string provenance
242+
string provenance, SourceOrSinkElement baseSink, string originalInput
169243
|
170-
sinkModel(namespace, type, subtypes, name, signature, ext, input, kind, provenance) and
244+
sinkModel(namespace, type, subtypes, name, signature, ext, originalInput, kind, provenance) and
171245
generated = isGenerated(provenance) and
172-
e = interpretElement(namespace, type, subtypes, name, signature, ext)
246+
baseSink = interpretElement(namespace, type, subtypes, name, signature, ext) and
247+
(
248+
e = baseSink and originalInput = input
249+
or
250+
correspondingKotlinParameterDefaultsArgSpec(baseSink, e, originalInput, input)
251+
)
173252
)
174253
}
175254

java/ql/test/kotlin/library-tests/parameter-defaults/defaults.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
| test.kt:5:3:7:3 | f | test.kt:5:3:7:3 | f$default |
2+
| test.kt:19:3:22:3 | f | test.kt:19:3:22:3 | f$default |
23
| test.kt:34:14:36:3 | f | test.kt:34:14:36:3 | f$default |
4+
| test.kt:56:3:58:3 | test | test.kt:56:3:58:3 | test$default |
35
| test.kt:68:1:80:1 | TestConstructor | test.kt:68:1:80:1 | TestConstructor |
46
| test.kt:86:5:88:5 | f | test.kt:86:5:88:5 | f$default |
57
| test.kt:106:7:108:7 | f | test.kt:106:7:108:7 | f$default |

0 commit comments

Comments
 (0)