Skip to content

Commit ccb57f2

Browse files
authored
Merge pull request github#12804 from asgerf/rb/api-graphs-cached
Ruby: restrict join order of API graph predicates
2 parents d94ed1b + f4e8656 commit ccb57f2

File tree

7 files changed

+154
-30
lines changed

7 files changed

+154
-30
lines changed

ruby/ql/lib/codeql/ruby/ApiGraphs.qll

Lines changed: 141 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ module API {
9999
*/
100100
pragma[inline]
101101
DataFlow::Node getAValueReachableFromSource() {
102-
exists(DataFlow::LocalSourceNode src | Impl::use(this, src) |
103-
Impl::trackUseNode(src).flowsTo(result)
104-
)
102+
result = getAValueReachableFromSourceInline(this)
105103
}
106104

107105
/**
@@ -121,7 +119,10 @@ module API {
121119
* end
122120
* ```
123121
*/
124-
DataFlow::LocalSourceNode asSource() { Impl::use(this, result) }
122+
pragma[inline]
123+
DataFlow::LocalSourceNode asSource() {
124+
result = pragma[only_bind_out](this).(Node::Internal).asSourceInternal()
125+
}
125126

126127
/**
127128
* Gets a data-flow node where this value leaves the current codebase and flows into an
@@ -167,6 +168,7 @@ module API {
167168
/**
168169
* Gets a call to a method on the receiver represented by this API component.
169170
*/
171+
pragma[inline]
170172
DataFlow::CallNode getAMethodCall(string method) { result = this.getReturn(method).asSource() }
171173

172174
/**
@@ -177,15 +179,20 @@ module API {
177179
* - A submodule of a module
178180
* - An attribute of an object
179181
*/
180-
bindingset[m]
181-
bindingset[result]
182-
Node getMember(string m) { result = this.getASuccessor(Label::member(m)) }
182+
pragma[inline]
183+
Node getMember(string m) {
184+
result = pragma[only_bind_out](this).(Node::Internal).getMemberInternal(m)
185+
}
183186

184187
/**
185188
* Gets a node representing a member of this API component where the name of the member may
186189
* or may not be known statically.
187190
*/
188-
Node getAMember() { result = this.getASuccessor(Label::member(_)) }
191+
cached
192+
Node getAMember() {
193+
Impl::forceCachingInSameStage() and
194+
result = this.getASuccessor(Label::member(_))
195+
}
189196

190197
/**
191198
* Gets a node representing an instance of this API component, that is, an object whose
@@ -198,41 +205,54 @@ module API {
198205
* This predicate may have multiple results when there are multiple constructor calls invoking this API component.
199206
* Consider using `getAnInstantiation()` if there is a need to distinguish between individual constructor calls.
200207
*/
208+
pragma[inline]
201209
Node getInstance() { result = this.getASubclass().getReturn("new") }
202210

203211
/**
204212
* Gets a node representing a call to `method` on the receiver represented by this node.
205213
*/
214+
pragma[inline]
206215
MethodAccessNode getMethod(string method) {
207-
result = this.getASubclass().getASuccessor(Label::method(method))
216+
result = pragma[only_bind_out](this).(Node::Internal).getMethodInternal(method)
208217
}
209218

210219
/**
211220
* Gets a node representing the result of this call.
212221
*/
213-
Node getReturn() { result = this.getASuccessor(Label::return()) }
222+
pragma[inline]
223+
Node getReturn() { result = pragma[only_bind_out](this).(Node::Internal).getReturnInternal() }
214224

215225
/**
216226
* Gets a node representing the result of calling a method on the receiver represented by this node.
217227
*/
228+
pragma[inline]
218229
Node getReturn(string method) { result = this.getMethod(method).getReturn() }
219230

220231
/** Gets an API node representing the `n`th positional parameter. */
221-
pragma[nomagic]
222-
Node getParameter(int n) { result = this.getASuccessor(Label::parameter(n)) }
232+
cached
233+
Node getParameter(int n) {
234+
Impl::forceCachingInSameStage() and
235+
result = this.getASuccessor(Label::parameter(n))
236+
}
223237

224238
/** Gets an API node representing the given keyword parameter. */
225-
pragma[nomagic]
239+
cached
226240
Node getKeywordParameter(string name) {
241+
Impl::forceCachingInSameStage() and
227242
result = this.getASuccessor(Label::keywordParameter(name))
228243
}
229244

230245
/** Gets an API node representing the block parameter. */
231-
Node getBlock() { result = this.getASuccessor(Label::blockParameter()) }
246+
cached
247+
Node getBlock() {
248+
Impl::forceCachingInSameStage() and
249+
result = this.getASuccessor(Label::blockParameter())
250+
}
232251

233252
/**
234253
* Gets a `new` call to the function represented by this API component.
235254
*/
255+
pragma[inline]
236256
DataFlow::ExprNode getAnInstantiation() { result = this.getInstance().asSource() }
237257

238258
/**
@@ -255,12 +275,17 @@ module API {
255275
* ```
256276
* In the example above, `getMember("A").getAnImmediateSubclass()` will return uses of `B` only.
257277
*/
258-
Node getAnImmediateSubclass() { result = this.getASuccessor(Label::subclass()) }
278+
cached
279+
Node getAnImmediateSubclass() {
280+
Impl::forceCachingInSameStage() and result = this.getASuccessor(Label::subclass())
281+
}
259282

260283
/**
261284
* Gets a node representing the `content` stored on the base object.
262285
*/
286+
cached
263287
Node getContent(DataFlow::Content content) {
288+
Impl::forceCachingInSameStage() and
264289
result = this.getASuccessor(Label::content(content))
265290
}
266291

@@ -274,10 +299,16 @@ module API {
274299
}
275300

276301
/** Gets a node representing the instance field of the given `name`, which must include the `@` character. */
277-
Node getField(string name) { result = this.getContent(DataFlowPrivate::TFieldContent(name)) }
302+
cached
303+
Node getField(string name) {
304+
Impl::forceCachingInSameStage() and
305+
result = this.getContent(DataFlowPrivate::TFieldContent(name))
306+
}
278307

279308
/** Gets a node representing an element of this collection (known or unknown). */
309+
cached
280310
Node getAnElement() {
311+
Impl::forceCachingInSameStage() and
281312
result = this.getContents(any(DataFlow::ContentSet set | set.isAnyElement()))
282313
}
283314

@@ -337,7 +368,7 @@ module API {
337368
/**
338369
* Gets a textual representation of this element.
339370
*/
340-
abstract string toString();
371+
string toString() { none() }
341372

342373
/**
343374
* Gets a path of the given `length` from the root to this node.
@@ -363,6 +394,65 @@ module API {
363394
int getDepth() { result = Impl::distanceFromRoot(this) }
364395
}
365396

397+
/** Companion module to the `Node` class. */
398+
module Node {
399+
/**
400+
* INTERNAL USE ONLY.
401+
*
402+
* An API node, with some internal predicates exposed.
403+
*/
404+
class Internal extends Node {
405+
/**
406+
* INTERNAL USE ONLY.
407+
*
408+
* Same as `asSource()` but without join-order hints.
409+
*/
410+
cached
411+
DataFlow::LocalSourceNode asSourceInternal() {
412+
Impl::forceCachingInSameStage() and
413+
Impl::use(this, result)
414+
}
415+
416+
/**
417+
* Same as `getMember` but without join-order hints.
418+
*/
419+
cached
420+
Node getMemberInternal(string m) {
421+
Impl::forceCachingInSameStage() and
422+
result = this.getASuccessor(Label::member(m))
423+
}
424+
425+
/**
426+
* Same as `getMethod` but without join-order hints.
427+
*/
428+
cached
429+
MethodAccessNode getMethodInternal(string method) {
430+
Impl::forceCachingInSameStage() and
431+
result = this.getASubclass().getASuccessor(Label::method(method))
432+
}
433+
434+
/**
435+
* INTERNAL USE ONLY.
436+
*
437+
* Same as `getReturn()` but without join-order hints.
438+
*/
439+
cached
440+
Node getReturnInternal() {
441+
Impl::forceCachingInSameStage() and result = this.getASuccessor(Label::return())
442+
}
443+
}
444+
}
445+
446+
bindingset[node]
447+
pragma[inline_late]
448+
private DataFlow::Node getAValueReachableFromSourceInline(Node node) {
449+
exists(DataFlow::LocalSourceNode src, DataFlow::LocalSourceNode dst |
450+
Impl::use(node, pragma[only_bind_into](src)) and
451+
pragma[only_bind_into](dst) = Impl::trackUseNode(src) and
452+
dst.flowsTo(result)
453+
)
454+
}
455+
366456
/** The root node of an API graph. */
367457
class Root extends Node, Impl::MkRoot {
368458
override string toString() { result = "root" }
@@ -443,7 +533,10 @@ module API {
443533
* you should use `.getMember` on the parent module/class. For example, for nodes corresponding to the class `Gem::Version`,
444534
* use `getTopLevelMember("Gem").getMember("Version")`.
445535
*/
446-
Node getTopLevelMember(string m) { result = root().getMember(m) }
536+
cached
537+
Node getTopLevelMember(string m) {
538+
Impl::forceCachingInSameStage() and result = root().(Node::Internal).getMemberInternal(m)
539+
}
447540

448541
/**
449542
* Provides the actual implementation of API graphs, cached for performance.
@@ -469,6 +562,36 @@ module API {
469562
*/
470563
cached
471564
private module Impl {
565+
cached
566+
predicate forceCachingInSameStage() { any() }
567+
568+
cached
569+
predicate forceCachingBackref() {
570+
1 = 1
571+
or
572+
exists(getTopLevelMember(_))
573+
or
574+
exists(
575+
any(Node n)
576+
.(Node::Internal)
577+
.getMemberInternal("foo")
578+
.getAMember()
579+
.(Node::Internal)
580+
.getMethodInternal("foo")
581+
.(Node::Internal)
582+
.getReturnInternal()
583+
.getParameter(0)
584+
.getKeywordParameter("foo")
585+
.getBlock()
586+
.getAnImmediateSubclass()
587+
.getContent(_)
588+
.getField(_)
589+
.getAnElement()
590+
.(Node::Internal)
591+
.asSourceInternal()
592+
)
593+
}
594+
472595
cached
473596
newtype TApiNode =
474597
/** The root of the API graph. */

ruby/ql/lib/codeql/ruby/frameworks/ActiveRecord.qll

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ private API::Node activeRecordClassApiNode() {
4040
// In Rails applications `ApplicationRecord` typically extends `ActiveRecord::Base`, but we
4141
// treat it separately in case the `ApplicationRecord` definition is not in the database.
4242
API::getTopLevelMember("ApplicationRecord")
43-
].getASubclass()
43+
]
4444
}
4545

4646
/**
@@ -58,7 +58,7 @@ private API::Node activeRecordClassApiNode() {
5858
class ActiveRecordModelClass extends ClassDeclaration {
5959
ActiveRecordModelClass() {
6060
this.getSuperclassExpr() =
61-
activeRecordClassApiNode().getAValueReachableFromSource().asExpr().getExpr()
61+
activeRecordClassApiNode().getASubclass().getAValueReachableFromSource().asExpr().getExpr()
6262
}
6363

6464
// Gets the class declaration for this class and all of its super classes

ruby/ql/lib/codeql/ruby/frameworks/ActiveResource.qll

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ module ActiveResource {
1717
/**
1818
* An ActiveResource model class. This is any (transitive) subclass of ActiveResource.
1919
*/
20+
pragma[nomagic]
2021
private API::Node modelApiNode() {
21-
result = API::getTopLevelMember("ActiveResource").getMember("Base").getASubclass+()
22+
result = API::getTopLevelMember("ActiveResource").getMember("Base").getASubclass()
2223
}
2324

2425
/**

ruby/ql/lib/codeql/ruby/frameworks/ActiveStorage.qll

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ module ActiveStorage {
8585
// Class methods
8686
API::getTopLevelMember("ActiveStorage")
8787
.getMember("Blob")
88-
.getASubclass()
8988
.getAMethodCall(["create_after_unfurling!", "create_and_upload!"]),
9089
// Instance methods
9190
any(BlobInstance i, DataFlow::CallNode c |

ruby/ql/lib/codeql/ruby/frameworks/GraphQL.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ private class GraphqlRelayClassicMutationClass extends ClassDeclaration {
4242
this.getSuperclassExpr() =
4343
graphQlSchema()
4444
.getMember("RelayClassicMutation")
45-
.getASubclass*()
45+
.getASubclass()
4646
.getAValueReachableFromSource()
4747
.asExpr()
4848
.getExpr()

ruby/ql/lib/codeql/ruby/frameworks/Twirp.qll

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ module Twirp {
1818
*/
1919
class ServiceInstantiation extends DataFlow::CallNode {
2020
ServiceInstantiation() {
21-
this =
22-
API::getTopLevelMember("Twirp").getMember("Service").getASubclass().getAnInstantiation()
21+
this = API::getTopLevelMember("Twirp").getMember("Service").getAnInstantiation()
2322
}
2423

2524
/**
@@ -62,7 +61,7 @@ module Twirp {
6261
*/
6362
class ClientInstantiation extends DataFlow::CallNode {
6463
ClientInstantiation() {
65-
this = API::getTopLevelMember("Twirp").getMember("Client").getASubclass().getAnInstantiation()
64+
this = API::getTopLevelMember("Twirp").getMember("Client").getAnInstantiation()
6665
}
6766
}
6867

ruby/ql/lib/codeql/ruby/frameworks/data/internal/ApiGraphModelsSpecific.qll

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,18 @@ API::Node getExtraNodeFromType(string type) {
113113
|
114114
suffix = "!" and
115115
(
116-
result.asSource() = constRef
116+
result.(API::Node::Internal).asSourceInternal() = constRef
117117
or
118-
result.asSource() = constRef.getADescendentModule().getAnOwnModuleSelf()
118+
result.(API::Node::Internal).asSourceInternal() =
119+
constRef.getADescendentModule().getAnOwnModuleSelf()
119120
)
120121
or
121122
suffix = "" and
122123
(
123-
result.asSource() = constRef.getAMethodCall("new")
124+
result.(API::Node::Internal).asSourceInternal() = constRef.getAMethodCall("new")
124125
or
125-
result.asSource() = constRef.getADescendentModule().getAnInstanceSelf()
126+
result.(API::Node::Internal).asSourceInternal() =
127+
constRef.getADescendentModule().getAnInstanceSelf()
126128
)
127129
)
128130
or

0 commit comments

Comments
 (0)