Skip to content

Commit 222aa41

Browse files
authored
Merge pull request github#13938 from hmac/splat-flow-2
Ruby: More precise flow into splat parameters
2 parents 08ef31d + da8005d commit 222aa41

File tree

7 files changed

+774
-5
lines changed

7 files changed

+774
-5
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Flow between positional arguments and splat parameters (`*args`) is now tracked more precisely.

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowDispatch.qll

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ private module Cached {
440440
THashSplatArgumentPosition() or
441441
TSplatAllArgumentPosition() or
442442
TSplatArgumentPosition(int pos) { exists(Call c | c.getArgument(pos) instanceof SplatExpr) } or
443+
TSynthSplatArgumentPosition() or
443444
TAnyArgumentPosition() or
444445
TAnyKeywordArgumentPosition()
445446

@@ -473,6 +474,7 @@ private module Cached {
473474
exists(Parameter p | p.getPosition() = pos and p instanceof SplatParameter)
474475
} or
475476
TSynthSplatParameterPosition() or
477+
TSynthArgSplatParameterPosition() or
476478
TAnyParameterPosition() or
477479
TAnyKeywordParameterPosition()
478480
}
@@ -1295,6 +1297,9 @@ class ParameterPosition extends TParameterPosition {
12951297

12961298
predicate isSynthSplat() { this = TSynthSplatParameterPosition() }
12971299

1300+
// A fake position to indicate that this parameter node holds content from a synth arg splat node
1301+
predicate isSynthArgSplat() { this = TSynthArgSplatParameterPosition() }
1302+
12981303
predicate isSplatAll() { this = TSplatAllParameterPosition() }
12991304

13001305
predicate isSplat(int n) { this = TSplatParameterPosition(n) }
@@ -1332,6 +1337,8 @@ class ParameterPosition extends TParameterPosition {
13321337
or
13331338
this.isSynthSplat() and result = "synthetic *"
13341339
or
1340+
this.isSynthArgSplat() and result = "synthetic * (from *args)"
1341+
or
13351342
exists(int pos | this.isSplat(pos) and result = "* (position " + pos + ")")
13361343
}
13371344
}
@@ -1369,6 +1376,8 @@ class ArgumentPosition extends TArgumentPosition {
13691376

13701377
predicate isSplat(int n) { this = TSplatArgumentPosition(n) }
13711378

1379+
predicate isSynthSplat() { this = TSynthSplatArgumentPosition() }
1380+
13721381
/** Gets a textual representation of this position. */
13731382
string toString() {
13741383
this.isSelf() and result = "self"
@@ -1387,6 +1396,8 @@ class ArgumentPosition extends TArgumentPosition {
13871396
or
13881397
this.isSplatAll() and result = "*"
13891398
or
1399+
this.isSynthSplat() and result = "synthetic *"
1400+
or
13901401
exists(int pos | this.isSplat(pos) and result = "* (position " + pos + ")")
13911402
}
13921403
}
@@ -1418,8 +1429,12 @@ predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) {
14181429
or
14191430
ppos.isSplatAll() and apos.isSplatAll()
14201431
or
1432+
ppos.isSplatAll() and apos.isSynthSplat()
1433+
or
14211434
ppos.isSynthSplat() and apos.isSplatAll()
14221435
or
1436+
apos.isSynthSplat() and ppos.isSynthArgSplat()
1437+
or
14231438
// Exact splat match
14241439
exists(int n | apos.isSplat(n) and ppos.isSplat(n))
14251440
or

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPrivate.qll

Lines changed: 153 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,15 @@ private module Cached {
344344
exists(c.asCallable()) and // exclude library callables
345345
isParameterNode(_, c, any(ParameterPosition p | p.isPositional(_)))
346346
} or
347+
TSynthSplatArgParameterNode(DataFlowCallable c) {
348+
exists(c.asCallable()) and // exclude library callables
349+
isParameterNode(_, c, any(ParameterPosition p | p.isSplat(_)))
350+
} or
351+
TSynthSplatParameterElementNode(DataFlowCallable c, int n) {
352+
exists(c.asCallable()) and // exclude library callables
353+
isParameterNode(_, c, any(ParameterPosition p | p.isSplat(_))) and
354+
n in [0 .. 10]
355+
} or
347356
TExprPostUpdateNode(CfgNodes::ExprCfgNode n) {
348357
// filter out nodes that clearly don't need post-update nodes
349358
isNonConstantExpr(n) and
@@ -358,11 +367,18 @@ private module Cached {
358367
exists(Argument arg | arg.isArgumentOf(c, any(ArgumentPosition pos | pos.isKeyword(_))))
359368
or
360369
c.getAnArgument() instanceof CfgNodes::ExprNodes::PairCfgNode
370+
} or
371+
TSynthSplatArgumentNode(CfgNodes::ExprNodes::CallCfgNode c) {
372+
exists(Argument arg, ArgumentPosition pos | pos.isPositional(_) | arg.isArgumentOf(c, pos)) and
373+
not exists(Argument arg, ArgumentPosition pos | pos.isSplat(_) or pos.isSplatAll() |
374+
arg.isArgumentOf(c, pos)
375+
)
361376
}
362377

363378
class TSourceParameterNode =
364379
TNormalParameterNode or TBlockParameterNode or TSelfParameterNode or
365-
TSynthHashSplatParameterNode or TSynthSplatParameterNode;
380+
TSynthHashSplatParameterNode or TSynthSplatParameterNode or TSynthSplatArgParameterNode or
381+
TSynthSplatParameterElementNode;
366382

367383
cached
368384
Location getLocation(NodeImpl n) { result = n.getLocationImpl() }
@@ -562,6 +578,12 @@ predicate nodeIsHidden(Node n) {
562578
n instanceof SynthHashSplatArgumentNode
563579
or
564580
n instanceof SynthSplatParameterNode
581+
or
582+
n instanceof SynthSplatArgumentNode
583+
or
584+
n instanceof SynthSplatArgParameterNode
585+
or
586+
n instanceof SynthSplatParameterElementNode
565587
}
566588

567589
/** An SSA definition, viewed as a node in a data flow graph. */
@@ -885,6 +907,69 @@ private module ParameterNodes {
885907
final override string toStringImpl() { result = "synthetic *args" }
886908
}
887909

910+
/**
911+
* A node that holds all positional arguments passed in a call to `c`.
912+
* This is a mirror of the `SynthSplatArgumentNode` on the callable side.
913+
* See `SynthSplatArgumentNode` for more information.
914+
*/
915+
class SynthSplatArgParameterNode extends ParameterNodeImpl, TSynthSplatArgParameterNode {
916+
private DataFlowCallable callable;
917+
918+
SynthSplatArgParameterNode() { this = TSynthSplatArgParameterNode(callable) }
919+
920+
final override Parameter getParameter() { none() }
921+
922+
final override predicate isParameterOf(DataFlowCallable c, ParameterPosition pos) {
923+
c = callable and pos.isSynthArgSplat()
924+
}
925+
926+
final override CfgScope getCfgScope() { result = callable.asCallable() }
927+
928+
final override DataFlowCallable getEnclosingCallable() { result = callable }
929+
930+
final override Location getLocationImpl() { result = callable.getLocation() }
931+
932+
final override string toStringImpl() { result = "synthetic *args" }
933+
}
934+
935+
/**
936+
* A node that holds the content of a specific positional argument.
937+
* See `SynthSplatArgumentNode` for more information.
938+
*/
939+
class SynthSplatParameterElementNode extends ParameterNodeImpl, TSynthSplatParameterElementNode {
940+
private DataFlowCallable callable;
941+
private int pos;
942+
943+
SynthSplatParameterElementNode() { this = TSynthSplatParameterElementNode(callable, pos) }
944+
945+
pragma[nomagic]
946+
NormalParameterNode getSplatParameterNode(int splatPos) {
947+
result
948+
.isParameterOf(this.getEnclosingCallable(), any(ParameterPosition p | p.isSplat(splatPos)))
949+
}
950+
951+
int getStorePosition() { result = pos }
952+
953+
int getReadPosition() {
954+
exists(int splatPos |
955+
exists(this.getSplatParameterNode(splatPos)) and
956+
result = pos + splatPos
957+
)
958+
}
959+
960+
final override Parameter getParameter() { none() }
961+
962+
final override predicate isParameterOf(DataFlowCallable c, ParameterPosition p) { none() }
963+
964+
final override CfgScope getCfgScope() { result = callable.asCallable() }
965+
966+
final override DataFlowCallable getEnclosingCallable() { result = callable }
967+
968+
final override Location getLocationImpl() { result = callable.getLocation() }
969+
970+
final override string toStringImpl() { result = "synthetic *args[" + pos + "]" }
971+
}
972+
888973
/** A parameter for a library callable with a flow summary. */
889974
class SummaryParameterNode extends ParameterNodeImpl, FlowSummaryNode {
890975
private ParameterPosition pos_;
@@ -1030,6 +1115,51 @@ private module ArgumentNodes {
10301115

10311116
override string toStringImpl() { result = "**" }
10321117
}
1118+
1119+
/**
1120+
* A data-flow node that represents all arguments passed to the call.
1121+
* We use this to model data flow via splat parameters.
1122+
* Consider this example:
1123+
*
1124+
* ```rb
1125+
* def foo(x, y, *z)
1126+
* end
1127+
*
1128+
* foo(1, 2, 3, 4)
1129+
* ```
1130+
*
1131+
* 1. We want `3` to flow to `z[0]` and `4` to flow to `z[1]`. We model this by first storing all arguments
1132+
* in a synthetic argument node `SynthSplatArgumentNode` (see `storeStepCommon`).
1133+
* 2. We match this to an analogous parameter node `SynthSplatArgParameterNode` on the callee side
1134+
* (see `parameterMatch`).
1135+
* 3. For each content element stored in the `SynthSplatArgParameterNode`, we add a read step to a separate
1136+
* `SynthSplatParameterElementNode`, which is parameterized by the element index (see `readStep`).
1137+
* 4. Finally, we add store steps from these `SynthSplatParameterElementNode`s to the real splat parameter node
1138+
* (see `storeStep`).
1139+
* We only add store steps for elements that will not flow to the earlier positional parameters.
1140+
* In practice that means we ignore elements at index `<= N`, where `N` is the index of the splat parameter.
1141+
* For the remaining elements we subtract `N` from their index and store them in the splat parameter.
1142+
*/
1143+
class SynthSplatArgumentNode extends ArgumentNode, NodeImpl, TSynthSplatArgumentNode {
1144+
CfgNodes::ExprNodes::CallCfgNode c;
1145+
1146+
SynthSplatArgumentNode() { this = TSynthSplatArgumentNode(c) }
1147+
1148+
override predicate argumentOf(DataFlowCall call, ArgumentPosition pos) {
1149+
this.sourceArgumentOf(call.asCall(), pos)
1150+
}
1151+
1152+
override predicate sourceArgumentOf(CfgNodes::ExprNodes::CallCfgNode call, ArgumentPosition pos) {
1153+
call = c and
1154+
pos.isSynthSplat()
1155+
}
1156+
1157+
override CfgScope getCfgScope() { result = c.getExpr().getCfgScope() }
1158+
1159+
override Location getLocationImpl() { result = c.getLocation() }
1160+
1161+
override string toStringImpl() { result = "*" }
1162+
}
10331163
}
10341164

10351165
import ArgumentNodes
@@ -1269,6 +1399,19 @@ predicate storeStepCommon(Node node1, ContentSet c, Node node2) {
12691399
c.isSingleton(TKnownElementContent(cv))
12701400
)
12711401
)
1402+
or
1403+
exists(CfgNodes::ExprNodes::CallCfgNode call, ArgumentPosition pos |
1404+
node2 = TSynthSplatArgumentNode(call) and
1405+
node1.asExpr().(Argument).isArgumentOf(call, pos)
1406+
|
1407+
exists(int n | pos.isPositional(n) and c = getPositionalContent(n))
1408+
)
1409+
or
1410+
node1 =
1411+
any(SynthSplatParameterElementNode elemNode |
1412+
node2 = elemNode.getSplatParameterNode(_) and
1413+
c = getPositionalContent(elemNode.getStorePosition())
1414+
)
12721415
}
12731416

12741417
/**
@@ -1334,10 +1477,17 @@ predicate readStep(Node node1, ContentSet c, Node node2) {
13341477
or
13351478
node2 = node1.(SynthHashSplatParameterNode).getAKeywordParameter(c)
13361479
or
1337-
node2 = node1.(SynthSplatParameterNode).getAParameter(c)
1338-
or
13391480
FlowSummaryImpl::Private::Steps::summaryReadStep(node1.(FlowSummaryNode).getSummaryNode(), c,
13401481
node2.(FlowSummaryNode).getSummaryNode())
1482+
or
1483+
node2 = node1.(SynthSplatParameterNode).getAParameter(c)
1484+
or
1485+
// Read from SynthSplatArgParameterNode into SynthSplatParameterElementNode
1486+
node2 =
1487+
any(SynthSplatParameterElementNode e |
1488+
node1.(SynthSplatArgParameterNode).isParameterOf(e.getEnclosingCallable(), _) and
1489+
c = getPositionalContent(e.getReadPosition())
1490+
)
13411491
}
13421492

13431493
/**

0 commit comments

Comments
 (0)