Skip to content

Commit 818753e

Browse files
authored
Merge pull request #13265 from asgerf/rb/delete-name-clash
Ruby: fix some name clashes between summarized callables
2 parents 796e71f + 8bd6f6c commit 818753e

File tree

5 files changed

+129
-6
lines changed

5 files changed

+129
-6
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* @name Name clash in summarized callable
3+
* @description Two summarized callables with the same name may apply to each others' call sites
4+
* @kind problem
5+
* @problem.severity warning
6+
* @id ql/name-clash-in-summarized-callable
7+
* @tags correctness
8+
* maintainability
9+
* @precision high
10+
*/
11+
12+
import ql
13+
14+
/** A non-abstract subclass of `SummarizedCallable`. */
15+
class SummarizedCallableImpl extends Class {
16+
SummarizedCallableImpl() {
17+
this.getType().getASuperType+().getName() = "SummarizedCallable" and
18+
not this.isAbstract()
19+
}
20+
21+
/** Gets an expression bound to `this` in the charpred. */
22+
Expr getAThisBoundExpr() {
23+
exists(ThisAccess thisExpr |
24+
thisExpr.getEnclosingPredicate() = this.getCharPred() and
25+
any(ComparisonFormula eq | eq.getOperator() = "=").hasOperands(thisExpr, result)
26+
)
27+
}
28+
29+
/** Gets a string value bound to `this` in the charpred. */
30+
string getAThisBoundString() { result = getStringValue(this.getAThisBoundExpr()) }
31+
32+
/** Holds if this class appears to apply call site filtering. */
33+
predicate hasConditions() {
34+
exists(Conjunction expr | expr.getEnclosingPredicate() = this.getCharPred())
35+
or
36+
exists(this.getClassPredicate(["getACall", "getACallSimple"]))
37+
}
38+
}
39+
40+
/** Holds if we should compute the string values of `e`. */
41+
predicate needsStringValue(Expr e) {
42+
e = any(SummarizedCallableImpl impl).getAThisBoundExpr()
43+
or
44+
exists(Expr parent | needsStringValue(parent) |
45+
e = parent.(BinOpExpr).getAnOperand()
46+
or
47+
e = parent.(Set).getAnElement()
48+
)
49+
}
50+
51+
/** Gets the string values of `e`. */
52+
string getStringValue(Expr e) {
53+
needsStringValue(e) and
54+
(
55+
result = e.(String).getValue()
56+
or
57+
exists(BinOpExpr op |
58+
e = op and
59+
op.getOperator() = "+" and
60+
result = getStringValue(op.getLeftOperand()) + getStringValue(op.getRightOperand())
61+
)
62+
or
63+
result = getStringValue(e.(Set).getAnElement())
64+
)
65+
}
66+
67+
/** Gets the enclosing `qlpack.yml` file in `folder` */
68+
File getQLPackFromFolder(Folder folder) {
69+
result = folder.getFile("qlpack.yml")
70+
or
71+
not exists(folder.getFile("qlpack.yml")) and
72+
result = getQLPackFromFolder(folder.getParentContainer())
73+
}
74+
75+
/** Gets a summarised callables in the given qlpack with the given this-value */
76+
SummarizedCallableImpl getASummarizedCallableByNameAndPack(string name, File qlpack) {
77+
name = result.getAThisBoundString() and
78+
qlpack = getQLPackFromFolder(result.getFile().getParentContainer())
79+
}
80+
81+
/** Holds if the given classes have a name clash. */
82+
predicate hasClash(SummarizedCallableImpl class1, SummarizedCallableImpl class2, string name) {
83+
exists(File qlpack |
84+
class1 = getASummarizedCallableByNameAndPack(name, qlpack) and
85+
class2 = getASummarizedCallableByNameAndPack(name, qlpack) and
86+
class1 != class2 and
87+
class1.hasConditions()
88+
|
89+
// One of the classes is unconditional, implying that it disables the condition in the other
90+
not class2.hasConditions()
91+
or
92+
// Always report classes from different files, as it is considered too subtle of an interaction.
93+
class1.getFile() != class2.getFile()
94+
)
95+
}
96+
97+
/** Like `hasClash` but tries to avoid duplicates. */
98+
predicate hasClashBreakSymmetry(
99+
SummarizedCallableImpl class1, SummarizedCallableImpl class2, string name
100+
) {
101+
hasClash(class1, class2, name) and
102+
hasClash(class2, class1, name) and
103+
// try to break symmetry arbitrarily
104+
class1.getName() <= class2.getName()
105+
or
106+
hasClash(class1, class2, name) and
107+
not hasClash(class2, class1, name)
108+
}
109+
110+
from SummarizedCallableImpl class1, SummarizedCallableImpl class2, string name
111+
where hasClashBreakSymmetry(class1, class2, name)
112+
select class1,
113+
class1.getName() + " and $@ both bind 'this' to the string \"" + name +
114+
"\". They may accidentally apply to each others' call sites.", class2, class2.getName()

ruby/ql/lib/codeql/ruby/frameworks/core/Array.qll

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,8 @@ module Array {
583583

584584
private class DeleteUnknownSummary extends DeleteSummary {
585585
DeleteUnknownSummary() {
586-
this = "delete" and
586+
// Note: take care to avoid a name clash with the "delete" summary from String.qll
587+
this = "delete-unknown-key" and
587588
not exists(DataFlow::Content::getKnownElementIndex(mc.getArgument(0)))
588589
}
589590

ruby/ql/lib/codeql/ruby/frameworks/core/Hash.qll

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,13 @@ module Hash {
199199
}
200200
}
201201

202-
private class AssocUnknownSummary extends AssocSummary {
203-
AssocUnknownSummary() {
204-
this = "assoc" and
205-
mc.getNumberOfArguments() = 1 and
206-
not exists(DataFlow::Content::getKnownElementIndex(mc.getArgument(0)))
202+
private class AssocUnknownSummary extends SummarizedCallable {
203+
AssocUnknownSummary() { this = "assoc-unknown-arg" }
204+
205+
override MethodCall getACallSimple() {
206+
result.getMethodName() = "assoc" and
207+
result.getNumberOfArguments() = 1 and
208+
not exists(DataFlow::Content::getKnownElementIndex(result.getArgument(0)))
207209
}
208210

209211
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Fixed an issue where calls to `delete` or `assoc` with a constant-valued argument would be analyzed imprecisely,
5+
as if the argument value was not a known constant.

ruby/ql/test/library-tests/dataflow/local/TaintStep.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2827,6 +2827,7 @@
28272827
| file://:0:0:0:0 | parameter self of ActiveSupportStringTransform | file://:0:0:0:0 | [summary] to write: return (return) in ActiveSupportStringTransform |
28282828
| file://:0:0:0:0 | parameter self of [] | file://:0:0:0:0 | [summary] to write: return (return) in [] |
28292829
| file://:0:0:0:0 | parameter self of \| | file://:0:0:0:0 | [summary] read: argument self.any element in \| |
2830+
| file://:0:0:0:0 | parameter self of assoc-unknown-arg | file://:0:0:0:0 | [summary] read: argument self.any element in assoc-unknown-arg |
28302831
| file://:0:0:0:0 | parameter self of each(0) | file://:0:0:0:0 | [summary] read: argument self.any element in each(0) |
28312832
| local_dataflow.rb:1:1:7:3 | self (foo) | local_dataflow.rb:3:8:3:10 | self |
28322833
| local_dataflow.rb:1:1:7:3 | self in foo | local_dataflow.rb:1:1:7:3 | self (foo) |

0 commit comments

Comments
 (0)