Skip to content

Commit 3ccc3a2

Browse files
committed
Ruby: move special treatment of Hash.[] into Hash.qll
1 parent 94d41b9 commit 3ccc3a2

File tree

2 files changed

+78
-30
lines changed

2 files changed

+78
-30
lines changed

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/** Provides flow summaries for the `Hash` class. */
22

33
private import codeql.ruby.AST
4+
private import codeql.ruby.CFG as Cfg
45
private import codeql.ruby.ApiGraphs
56
private import codeql.ruby.DataFlow
67
private import codeql.ruby.dataflow.FlowSummary
78
private import codeql.ruby.dataflow.internal.DataFlowDispatch
89
private import codeql.ruby.ast.internal.Module
10+
private import codeql.ruby.typetracking.TypeTrackerSpecific
911

1012
/**
1113
* Provides flow summaries for the `Hash` class.
@@ -68,6 +70,50 @@ module Hash {
6870
}
6971
}
7072

73+
/** Holds if `literal` is a call to `Hash.[]` and `argument` is one of its arguments. */
74+
private predicate hashLiteralStore(DataFlow::CallNode literal, DataFlow::Node argument) {
75+
literal.getExprNode().getExpr() = Hash::getAStaticHashCall("[]") and
76+
argument = literal.getArgument(_)
77+
}
78+
79+
/**
80+
* A set of type-tracking steps to replace the `Hash.[]` summary.
81+
*
82+
* The `Hash.[]` method tends to have a large number of summaries, which would result
83+
* in too many unnecessary type-tracking edges, so we specialize it here.
84+
*/
85+
private class HashLiteralTypeTracker extends TypeTrackingStep {
86+
override predicate suppressSummary(SummarizedCallable callable) {
87+
callable instanceof HashLiteralSummary
88+
}
89+
90+
override predicate storeStep(Node pred, TypeTrackingNode succ, TypeTrackerContent content) {
91+
// Store edge: `value -> { key: value }` with content derived from `key`
92+
exists(Cfg::CfgNodes::ExprNodes::PairCfgNode pair |
93+
hashLiteralStore(succ, any(DataFlow::Node n | n.asExpr() = pair)) and
94+
pred.asExpr() = pair.getValue()
95+
|
96+
exists(ConstantValue constant |
97+
constant = pair.getKey().getConstantValue() and
98+
content.isSingleton(DataFlow::Content::getElementContent(constant))
99+
)
100+
or
101+
not exists(pair.getKey().getConstantValue()) and
102+
content.isAnyElement()
103+
)
104+
}
105+
106+
override predicate withContentStep(Node pred, Node succ, ContentFilter filter) {
107+
// `WithContent[element]` edge: `args --> { **args }`.
108+
exists(DataFlow::Node node |
109+
hashLiteralStore(succ, node) and
110+
node.asExpr().getExpr() instanceof HashSplatExpr and
111+
pred.asExpr() = node.asExpr().(Cfg::CfgNodes::ExprNodes::UnaryOperationCfgNode).getOperand() and
112+
filter = ContentFilter::hasElements()
113+
)
114+
}
115+
}
116+
71117
/**
72118
* `Hash[]` called on an existing hash, e.g.
73119
*

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

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ private import codeql.ruby.dataflow.internal.SsaImpl as SsaImpl
1010
private import codeql.ruby.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
1111
private import codeql.ruby.dataflow.internal.FlowSummaryImplSpecific as FlowSummaryImplSpecific
1212
private import codeql.ruby.dataflow.internal.AccessPathSyntax
13-
private import codeql.ruby.frameworks.core.Hash
1413

1514
class Node = DataFlowPublic::Node;
1615

@@ -61,6 +60,15 @@ class ContentFilter extends TContentFilter {
6160
}
6261
}
6362

63+
/** Module for getting `ContentFilter` values. */
64+
module ContentFilter {
65+
/** Gets the filter that only allow element contents. */
66+
ContentFilter hasElements() { result = MkElementFilter() }
67+
68+
/** Gets the filter that only allow pair-value contents. */
69+
ContentFilter hasPairValue() { result = MkPairValueFilter() }
70+
}
71+
6472
/**
6573
* Holds if a value stored with `storeContents` can be read back with `loadContents`.
6674
*/
@@ -226,28 +234,9 @@ predicate basicStoreStep(Node nodeFrom, Node nodeTo, DataFlow::ContentSet conten
226234
nodeTo = evaluateSummaryComponentStackLocal(callable, call, output)
227235
)
228236
or
229-
// Hash literals
230-
exists(Cfg::CfgNodes::ExprNodes::PairCfgNode pair |
231-
hashLiteralStore(nodeTo, any(DataFlow::Node n | n.asExpr() = pair)) and
232-
nodeFrom.asExpr() = pair.getValue()
233-
|
234-
exists(ConstantValue constant |
235-
constant = pair.getKey().getConstantValue() and
236-
contents.isSingleton(DataFlow::Content::getElementContent(constant))
237-
)
238-
or
239-
not exists(pair.getKey().getConstantValue()) and
240-
contents.isAnyElement()
241-
)
242-
or
243237
TypeTrackingStep::storeStep(nodeFrom, nodeTo, contents)
244238
}
245239

246-
private predicate hashLiteralStore(DataFlow::CallNode hashCreation, DataFlow::Node argument) {
247-
hashCreation.getExprNode().getExpr() = Hash::getAStaticHashCall("[]") and
248-
argument = hashCreation.getArgument(_)
249-
}
250-
251240
/**
252241
* Holds if a store step `nodeFrom -> nodeTo` with `contents` exists, where the destination node
253242
* is a post-update node that should be treated as a local source node.
@@ -343,14 +332,6 @@ predicate basicWithContentStep(Node nodeFrom, Node nodeTo, ContentFilter filter)
343332
nodeTo = evaluateSummaryComponentStackLocal(callable, call, output)
344333
)
345334
or
346-
// Hash-splat in a hash literal
347-
exists(DataFlow::Node node |
348-
hashLiteralStore(nodeTo, node) and
349-
node.asExpr().getExpr() instanceof HashSplatExpr and
350-
nodeFrom.asExpr() = node.asExpr().(Cfg::CfgNodes::ExprNodes::UnaryOperationCfgNode).getOperand() and
351-
filter = MkElementFilter()
352-
)
353-
or
354335
TypeTrackingStep::withContentStep(nodeFrom, nodeTo, filter)
355336
}
356337

@@ -368,6 +349,7 @@ private predicate hasStoreSummary(
368349
SummarizedCallable callable, DataFlow::ContentSet contents, SummaryComponentStack input,
369350
SummaryComponentStack output
370351
) {
352+
not TypeTrackingStep::suppressSummary(callable) and
371353
callable.propagatesFlow(input, push(SummaryComponent::content(contents), output), true) and
372354
not isNonLocal(input.head()) and
373355
not isNonLocal(output.head())
@@ -378,6 +360,7 @@ private predicate hasLoadSummary(
378360
SummarizedCallable callable, DataFlow::ContentSet contents, SummaryComponentStack input,
379361
SummaryComponentStack output
380362
) {
363+
not TypeTrackingStep::suppressSummary(callable) and
381364
callable.propagatesFlow(push(SummaryComponent::content(contents), input), output, true) and
382365
not isNonLocal(input.head()) and
383366
not isNonLocal(output.head())
@@ -388,12 +371,12 @@ private predicate hasLoadStoreSummary(
388371
SummarizedCallable callable, DataFlow::ContentSet loadContents,
389372
DataFlow::ContentSet storeContents, SummaryComponentStack input, SummaryComponentStack output
390373
) {
374+
not TypeTrackingStep::suppressSummary(callable) and
391375
callable
392376
.propagatesFlow(push(SummaryComponent::content(loadContents), input),
393377
push(SummaryComponent::content(storeContents), output), true) and
394378
not isNonLocal(input.head()) and
395-
not isNonLocal(output.head()) and
396-
callable != "Hash.[]" // Special-cased due to having a huge number of summaries
379+
not isNonLocal(output.head())
397380
}
398381

399382
/**
@@ -426,6 +409,7 @@ private predicate hasWithoutContentSummary(
426409
SummaryComponentStack output
427410
) {
428411
exists(DataFlow::ContentSet content |
412+
not TypeTrackingStep::suppressSummary(callable) and
429413
callable.propagatesFlow(push(SummaryComponent::withoutContent(content), input), output, true) and
430414
filter = getFilterFromWithoutContentStep(content) and
431415
not isNonLocal(input.head()) and
@@ -464,6 +448,7 @@ private predicate hasWithContentSummary(
464448
SummaryComponentStack output
465449
) {
466450
exists(DataFlow::ContentSet content |
451+
not TypeTrackingStep::suppressSummary(callable) and
467452
callable.propagatesFlow(push(SummaryComponent::withContent(content), input), output, true) and
468453
filter = getFilterFromWithContentStep(content) and
469454
not isNonLocal(input.head()) and
@@ -508,6 +493,7 @@ private predicate dependsOnSummaryComponentStack(
508493
SummarizedCallable callable, SummaryComponentStack stack
509494
) {
510495
exists(callable.getACallSimple()) and
496+
not TypeTrackingStep::suppressSummary(callable) and
511497
(
512498
callable.propagatesFlow(stack, _, true)
513499
or
@@ -592,6 +578,13 @@ class TypeTrackingStep extends TUnit {
592578
/** Gets the string `"unit"`. */
593579
string toString() { result = "unit" }
594580

581+
/**
582+
* Holds if type-tracking should not attempt to derive steps from (simple) calls to `callable`.
583+
*
584+
* This can be done to manually control how steps are generated from such calls.
585+
*/
586+
predicate suppressSummary(SummarizedCallable callable) { none() }
587+
595588
/**
596589
* Holds if type-tracking should step from `pred` to `succ`.
597590
*/
@@ -630,6 +623,15 @@ class TypeTrackingStep extends TUnit {
630623

631624
/** Provides access to the steps contributed by subclasses of `SharedTypeTrackingStep`. */
632625
module TypeTrackingStep {
626+
/**
627+
* Holds if type-tracking should not attempt to derive steps from (simple) calls to `callable`.
628+
*
629+
* This can be done to manually control how steps are generated from such calls.
630+
*/
631+
predicate suppressSummary(SummarizedCallable callable) {
632+
any(TypeTrackingStep st).suppressSummary(callable)
633+
}
634+
633635
/**
634636
* Holds if type-tracking should step from `pred` to `succ`.
635637
*/

0 commit comments

Comments
 (0)