Skip to content

Commit dc2acf5

Browse files
authored
Merge pull request github#14090 from hmac/splat-flow-4
Ruby: More splat flow (alternative)
2 parents f7daa86 + 4168245 commit dc2acf5

File tree

8 files changed

+1369
-130
lines changed

8 files changed

+1369
-130
lines changed

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,6 @@ private module Cached {
472472
exists(Parameter p | p.getPosition() = pos and p instanceof SplatParameter)
473473
} or
474474
TSynthSplatParameterPosition() or
475-
TSynthArgSplatParameterPosition() or
476475
TAnyParameterPosition() or
477476
TAnyKeywordParameterPosition()
478477
}
@@ -1302,9 +1301,6 @@ class ParameterPosition extends TParameterPosition {
13021301

13031302
predicate isSynthSplat() { this = TSynthSplatParameterPosition() }
13041303

1305-
// A fake position to indicate that this parameter node holds content from a synth arg splat node
1306-
predicate isSynthArgSplat() { this = TSynthArgSplatParameterPosition() }
1307-
13081304
predicate isSplat(int n) { this = TSplatParameterPosition(n) }
13091305

13101306
/**
@@ -1340,8 +1336,6 @@ class ParameterPosition extends TParameterPosition {
13401336
or
13411337
this.isSynthSplat() and result = "synthetic *"
13421338
or
1343-
this.isSynthArgSplat() and result = "synthetic * (from *args)"
1344-
or
13451339
exists(int pos | this.isSplat(pos) and result = "* (position " + pos + ")")
13461340
}
13471341
}
@@ -1441,9 +1435,8 @@ predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) {
14411435
or
14421436
ppos.isSplat(0) and apos.isSynthSplat()
14431437
or
1444-
ppos.isSynthSplat() and apos.isSplat(0)
1445-
or
1446-
apos.isSynthSplat() and ppos.isSynthArgSplat()
1438+
ppos.isSynthSplat() and
1439+
(apos.isSynthSplat() or apos.isSplat(0))
14471440
or
14481441
// Exact splat match
14491442
exists(int n | apos.isSplat(n) and ppos.isSplat(n))

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

Lines changed: 123 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -455,13 +455,9 @@ private module Cached {
455455
exists(c.asCallable()) and // exclude library callables
456456
isParameterNode(_, c, any(ParameterPosition p | p.isPositional(_)))
457457
} or
458-
TSynthSplatArgParameterNode(DataFlowCallable c) {
459-
exists(c.asCallable()) and // exclude library callables
460-
isParameterNode(_, c, any(ParameterPosition p | p.isSplat(_)))
461-
} or
462458
TSynthSplatParameterElementNode(DataFlowCallable c, int n) {
463459
exists(c.asCallable()) and // exclude library callables
464-
isParameterNode(_, c, any(ParameterPosition p | p.isSplat(_))) and
460+
isParameterNode(_, c, any(ParameterPosition p | p.isSplat(any(int i | i > 0)))) and
465461
n in [0 .. 10]
466462
} or
467463
TExprPostUpdateNode(CfgNodes::ExprCfgNode n) {
@@ -479,15 +475,19 @@ private module Cached {
479475
or
480476
c.getAnArgument() instanceof CfgNodes::ExprNodes::PairCfgNode
481477
} or
482-
TSynthSplatArgumentNode(CfgNodes::ExprNodes::CallCfgNode c) {
483-
exists(Argument arg, ArgumentPosition pos | pos.isPositional(_) | arg.isArgumentOf(c, pos)) and
484-
not exists(Argument arg, ArgumentPosition pos | pos.isSplat(_) | arg.isArgumentOf(c, pos))
478+
TSynthSplatArgumentNode(CfgNodes::ExprNodes::CallCfgNode c) or
479+
TSynthSplatArgumentElementNode(CfgNodes::ExprNodes::CallCfgNode c, int n) {
480+
// we use -1 to represent data at an unknown index
481+
n in [-1 .. 10] and
482+
exists(Argument arg, ArgumentPosition pos |
483+
pos.isSplat(any(int p | p > 0)) and arg.isArgumentOf(c, pos)
484+
)
485485
} or
486486
TCaptureNode(VariableCapture::Flow::SynthesizedCaptureNode cn)
487487

488488
class TSourceParameterNode =
489489
TNormalParameterNode or TBlockParameterNode or TSelfParameterNode or
490-
TSynthHashSplatParameterNode or TSynthSplatParameterNode or TSynthSplatArgParameterNode;
490+
TSynthHashSplatParameterNode or TSynthSplatParameterNode;
491491

492492
cached
493493
Location getLocation(NodeImpl n) { result = n.getLocationImpl() }
@@ -695,8 +695,6 @@ predicate nodeIsHidden(Node n) {
695695
or
696696
n instanceof SynthSplatArgumentNode
697697
or
698-
n instanceof SynthSplatArgParameterNode
699-
or
700698
n instanceof SynthSplatParameterElementNode
701699
or
702700
n instanceof LambdaSelfReferenceNode
@@ -1026,19 +1024,23 @@ private module ParameterNodes {
10261024
* For example, in the following code:
10271025
*
10281026
* ```rb
1029-
* def foo(x, y); end
1027+
* def foo(x, y, z); end
10301028
*
1031-
* foo(*[a, b])
1029+
* foo(a, *[b, c])
10321030
* ```
10331031
*
1034-
* We want `a` to flow to `x` and `b` to flow to `y`. We do this by constructing
1032+
* We want `b` to flow to `y` and `c` to flow to `z`. We do this by constructing
10351033
* a `SynthSplatParameterNode` for the method `foo`, and matching the splat argument to this
10361034
* parameter node via `parameterMatch/2`. We then add read steps from this node to parameters
1037-
* `x` and `y`, for content at indices 0 and 1 respectively (see `readStep`).
1035+
* `y` and `z`, for content at indices 0 and 1 respectively (see `readStep`).
1036+
*
1037+
* This node stores the index of the splat argument it is matched to, which allows us to shift
1038+
* indices correctly when adding read steps. Without this, in the example above we would erroneously
1039+
* get a read step to `x` at index 0 and `y` at index 1 etc.
10381040
*
1039-
* We don't yet correctly handle cases where the splat argument is not the first argument, e.g. in
1041+
* We don't yet correctly handle cases where a positional argument follows the splat argument, e.g. in
10401042
* ```rb
1041-
* foo(a, *[b])
1043+
* foo(a, *[b], c)
10421044
* ```
10431045
*/
10441046
class SynthSplatParameterNode extends ParameterNodeImpl, TSynthSplatParameterNode {
@@ -1047,16 +1049,16 @@ private module ParameterNodes {
10471049
SynthSplatParameterNode() { this = TSynthSplatParameterNode(callable) }
10481050

10491051
/**
1050-
* Gets a parameter which will contain the value given by `c`, assuming
1051-
* that the method was called with a single splat argument.
1052-
* For example, if the synth splat parameter is for the following method
1052+
* Gets a parameter which will contain the value given by `c`.
1053+
* For example, if the synth splat parameter is for the following method and method call:
10531054
*
10541055
* ```rb
1055-
* def foo(x, y, a:, *rest)
1056-
* end
1056+
* def foo(x, y, a:, *rest); end
1057+
*
1058+
* foo(arg1, *args)
10571059
* ```
10581060
*
1059-
* Then `getAParameter(element 0) = x` and `getAParameter(element 1) = y`.
1061+
* then `getAParameter(element 0) = y`.
10601062
*/
10611063
ParameterNode getAParameter(ContentSet c) {
10621064
exists(int n |
@@ -1084,31 +1086,6 @@ private module ParameterNodes {
10841086
final override string toStringImpl() { result = "synthetic *args" }
10851087
}
10861088

1087-
/**
1088-
* A node that holds all positional arguments passed in a call to `c`.
1089-
* This is a mirror of the `SynthSplatArgumentNode` on the callable side.
1090-
* See `SynthSplatArgumentNode` for more information.
1091-
*/
1092-
class SynthSplatArgParameterNode extends ParameterNodeImpl, TSynthSplatArgParameterNode {
1093-
private DataFlowCallable callable;
1094-
1095-
SynthSplatArgParameterNode() { this = TSynthSplatArgParameterNode(callable) }
1096-
1097-
final override Parameter getParameter() { none() }
1098-
1099-
final override predicate isParameterOf(DataFlowCallable c, ParameterPosition pos) {
1100-
c = callable and pos.isSynthArgSplat()
1101-
}
1102-
1103-
final override CfgScope getCfgScope() { result = callable.asCallable() }
1104-
1105-
final override DataFlowCallable getEnclosingCallable() { result = callable }
1106-
1107-
final override Location getLocationImpl() { result = callable.getLocation() }
1108-
1109-
final override string toStringImpl() { result = "synthetic *args" }
1110-
}
1111-
11121089
/**
11131090
* A node that holds the content of a specific positional argument.
11141091
* See `SynthSplatArgumentNode` for more information.
@@ -1127,12 +1104,7 @@ private module ParameterNodes {
11271104

11281105
int getStorePosition() { result = pos }
11291106

1130-
int getReadPosition() {
1131-
exists(int splatPos |
1132-
exists(this.getSplatParameterNode(splatPos)) and
1133-
result = pos + splatPos
1134-
)
1135-
}
1107+
int getReadPosition() { result = pos }
11361108

11371109
final override CfgScope getCfgScope() { result = callable.asCallable() }
11381110

@@ -1246,7 +1218,7 @@ module ArgumentNodes {
12461218
* part of the method signature, such that those cannot end up in the hash-splat
12471219
* parameter.
12481220
*/
1249-
class SynthHashSplatArgumentNode extends ArgumentNode, TSynthHashSplatArgumentNode {
1221+
class SynthHashSplatArgumentNode extends ArgumentNode, NodeImpl, TSynthHashSplatArgumentNode {
12501222
CfgNodes::ExprNodes::CallCfgNode c;
12511223

12521224
SynthHashSplatArgumentNode() { this = TSynthHashSplatArgumentNode(c) }
@@ -1259,12 +1231,6 @@ module ArgumentNodes {
12591231
call = c and
12601232
pos.isHashSplat()
12611233
}
1262-
}
1263-
1264-
private class SynthHashSplatArgumentNodeImpl extends NodeImpl, TSynthHashSplatArgumentNode {
1265-
CfgNodes::ExprNodes::CallCfgNode c;
1266-
1267-
SynthHashSplatArgumentNodeImpl() { this = TSynthHashSplatArgumentNode(c) }
12681234

12691235
override CfgScope getCfgScope() { result = c.getExpr().getCfgScope() }
12701236

@@ -1287,9 +1253,9 @@ module ArgumentNodes {
12871253
*
12881254
* 1. We want `3` to flow to `z[0]` and `4` to flow to `z[1]`. We model this by first storing all arguments
12891255
* in a synthetic argument node `SynthSplatArgumentNode` (see `storeStepCommon`).
1290-
* 2. We match this to an analogous parameter node `SynthSplatArgParameterNode` on the callee side
1256+
* 2. We match this to an analogous parameter node `SynthSplatParameterNode` on the callee side
12911257
* (see `parameterMatch`).
1292-
* 3. For each content element stored in the `SynthSplatArgParameterNode`, we add a read step to a separate
1258+
* 3. For each content element stored in the `SynthSplatParameterNode`, we add a read step to a separate
12931259
* `SynthSplatParameterElementNode`, which is parameterized by the element index (see `readStep`).
12941260
* 4. Finally, we add store steps from these `SynthSplatParameterElementNode`s to the real splat parameter node
12951261
* (see `storeStep`).
@@ -1317,6 +1283,33 @@ module ArgumentNodes {
13171283

13181284
override string toStringImpl() { result = "*" }
13191285
}
1286+
1287+
/**
1288+
* A data-flow node that holds data from values inside splat arguments.
1289+
* For example, in the following call
1290+
*
1291+
* ```rb
1292+
* foo(1, 2, *[3, 4])
1293+
* ```
1294+
*
1295+
* We add read steps such that `3` flows into `SynthSplatArgumentElementNode(2)` and `4` flows into `SynthSplatArgumentElementNode(3)`.
1296+
*/
1297+
class SynthSplatArgumentElementNode extends NodeImpl, TSynthSplatArgumentElementNode {
1298+
CfgNodes::ExprNodes::CallCfgNode c;
1299+
int n;
1300+
1301+
SynthSplatArgumentElementNode() { this = TSynthSplatArgumentElementNode(c, n) }
1302+
1303+
CfgNodes::ExprNodes::CallCfgNode getCall() { result = c }
1304+
1305+
int getPosition() { result = n }
1306+
1307+
override CfgScope getCfgScope() { result = c.getExpr().getCfgScope() }
1308+
1309+
override Location getLocationImpl() { result = c.getLocation() }
1310+
1311+
override string toStringImpl() { result = "*[" + n + "]" }
1312+
}
13201313
}
13211314

13221315
import ArgumentNodes
@@ -1556,6 +1549,33 @@ predicate storeStepCommon(Node node1, ContentSet c, Node node2) {
15561549
)
15571550
}
15581551

1552+
/**
1553+
* Holds if data can flow from a `SynthSplatArgumentElementNode` into a `SynthSplatArgumentNode` via a store step.
1554+
* For example in
1555+
*
1556+
* ```rb
1557+
* foo(1, 2, *[3, 4])
1558+
* ```
1559+
*
1560+
* We have flow from `3` into `SynthSplatArgumentElementNode(2)`. This step stores the value from this node into element `2` of the `SynthSplatArgumentNode`.
1561+
*
1562+
* This allows us to match values inside splat arguments to the correct parameter in the callable.
1563+
*/
1564+
predicate synthSplatArgumentElementStoreStep(
1565+
SynthSplatArgumentElementNode node1, ContentSet c, SynthSplatArgumentNode node2
1566+
) {
1567+
exists(CfgNodes::ExprNodes::CallCfgNode call, int n |
1568+
node2 = TSynthSplatArgumentNode(call) and
1569+
node1 = TSynthSplatArgumentElementNode(call, n) and
1570+
(
1571+
c = getPositionalContent(n)
1572+
or
1573+
n = -1 and
1574+
c.isSingleton(TUnknownElementContent())
1575+
)
1576+
)
1577+
}
1578+
15591579
/**
15601580
* Holds if data can flow from `node1` to `node2` via an assignment to
15611581
* content `c`.
@@ -1587,11 +1607,13 @@ predicate storeStep(Node node1, ContentSet c, Node node2) {
15871607
FlowSummaryImpl::Private::Steps::summaryStoreStep(node1.(FlowSummaryNode).getSummaryNode(), c,
15881608
node2.(FlowSummaryNode).getSummaryNode())
15891609
or
1590-
node1 =
1591-
any(SynthSplatParameterElementNode elemNode |
1592-
node2 = elemNode.getSplatParameterNode(_) and
1593-
c = getPositionalContent(elemNode.getStorePosition())
1594-
)
1610+
exists(SynthSplatParameterElementNode elemNode, int splatPos |
1611+
node1 = elemNode and
1612+
node2 = elemNode.getSplatParameterNode(splatPos) and
1613+
c = getPositionalContent(elemNode.getStorePosition() - splatPos)
1614+
)
1615+
or
1616+
synthSplatArgumentElementStoreStep(node1, c, node2)
15951617
or
15961618
storeStepCommon(node1, c, node2)
15971619
or
@@ -1608,6 +1630,34 @@ predicate readStepCommon(Node node1, ContentSet c, Node node2) {
16081630
node2 = node1.(SynthSplatParameterNode).getAParameter(c)
16091631
}
16101632

1633+
/**
1634+
* Holds if data can flow from a splat argument to a `SynthSplatArgumentElementNode` via a read step.
1635+
* For example in
1636+
* ```rb
1637+
* foo(x, y, *[1, 2])
1638+
* ```
1639+
*
1640+
* we read `1` into `SynthSplatArgumentElementNode(2)` and `2` into `SynthSplatArgumentElementNode(3)`.
1641+
*/
1642+
predicate synthSplatArgumentElementReadStep(
1643+
Node node1, ContentSet c, SynthSplatArgumentElementNode node2
1644+
) {
1645+
exists(int splatPos, CfgNodes::ExprNodes::CallCfgNode call |
1646+
node1.asExpr().(Argument).isArgumentOf(call, any(ArgumentPosition p | p.isSplat(splatPos))) and
1647+
splatPos > 0 and
1648+
node2.getCall() = call and
1649+
(
1650+
exists(int n |
1651+
node2.getPosition() = n + splatPos and
1652+
c = getPositionalContent(n)
1653+
)
1654+
or
1655+
node2.getPosition() = -1 and
1656+
c.isSingleton(TUnknownElementContent())
1657+
)
1658+
)
1659+
}
1660+
16111661
/**
16121662
* Holds if there is a read step of content `c` from `node1` to `node2`.
16131663
*/
@@ -1638,14 +1688,16 @@ predicate readStep(Node node1, ContentSet c, Node node2) {
16381688
FlowSummaryImpl::Private::Steps::summaryReadStep(node1.(FlowSummaryNode).getSummaryNode(), c,
16391689
node2.(FlowSummaryNode).getSummaryNode())
16401690
or
1641-
// Read from SynthSplatArgParameterNode into SynthSplatParameterElementNode
1691+
VariableCapture::readStep(node1, any(Content::CapturedVariableContent v | c.isSingleton(v)), node2)
1692+
or
1693+
// Read from SynthSplatParameterNode into SynthSplatParameterElementNode
16421694
node2 =
16431695
any(SynthSplatParameterElementNode e |
1644-
node1.(SynthSplatArgParameterNode).isParameterOf(e.getEnclosingCallable(), _) and
1696+
node1.(SynthSplatParameterNode).isParameterOf(e.getEnclosingCallable(), _) and
16451697
c = getPositionalContent(e.getReadPosition())
16461698
)
16471699
or
1648-
VariableCapture::readStep(node1, any(Content::CapturedVariableContent v | c.isSingleton(v)), node2)
1700+
synthSplatArgumentElementReadStep(node1, c, node2)
16491701
or
16501702
readStepCommon(node1, c, node2)
16511703
}

ruby/ql/lib/codeql/ruby/typetracking/TypeTrackerSpecific.qll

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,13 +334,16 @@ predicate readStoreStepIntoSourceNode(
334334
Node nodeFrom, Node nodeTo, DataFlow::ContentSet loadContent, DataFlow::ContentSet storeContent
335335
) {
336336
exists(DataFlowPrivate::SynthSplatParameterElementNode mid |
337-
nodeFrom
338-
.(DataFlowPrivate::SynthSplatArgParameterNode)
339-
.isParameterOf(mid.getEnclosingCallable(), _) and
337+
nodeFrom.(DataFlowPrivate::SynthSplatParameterNode).isParameterOf(mid.getEnclosingCallable(), _) and
340338
loadContent = DataFlowPrivate::getPositionalContent(mid.getReadPosition()) and
341339
nodeTo = mid.getSplatParameterNode(_) and
342340
storeContent = DataFlowPrivate::getPositionalContent(mid.getStorePosition())
343341
)
342+
or
343+
exists(DataFlowPrivate::SynthSplatArgumentElementNode mid |
344+
DataFlowPrivate::synthSplatArgumentElementReadStep(nodeFrom, loadContent, mid) and
345+
DataFlowPrivate::synthSplatArgumentElementStoreStep(mid, storeContent, nodeTo)
346+
)
344347
}
345348

346349
/**

0 commit comments

Comments
 (0)