Skip to content

Commit 5e551dd

Browse files
alexmarkovcommit-bot@chromium.org
authored andcommitted
[vm/aot/tfa] Avoid stack overflow in TFA due to deeply nested calls
Issue: flutter/flutter#63560 Change-Id: I995adf4c7ee17123f9a4979e07e651911c9577db Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/158720 Commit-Queue: Alexander Markov <[email protected]> Reviewed-by: Aske Simon Christensen <[email protected]> Reviewed-by: Vyacheslav Egorov <[email protected]>
1 parent 2b7fc8f commit 5e551dd

File tree

5 files changed

+2155
-29
lines changed

5 files changed

+2155
-29
lines changed

pkg/vm/lib/transformations/type_flow/analysis.dart

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,36 @@ abstract class _Invocation extends _DependencyTracker
108108
/// Used for recursive calls while this invocation is being processed.
109109
Type get resultForRecursiveInvocation => result;
110110

111+
/// Use [type] as a current computed result of this invocation.
112+
/// If this invocation was invalidated, and the invalidated result is
113+
/// different, then invalidate all dependent invocations as well.
114+
/// Result type may be saturated if this invocation was invalidated
115+
/// too many times.
116+
void setResult(TypeFlowAnalysis typeFlowAnalysis, Type type) {
117+
assertx(type != null);
118+
result = type;
119+
120+
if (invalidatedResult != null) {
121+
if (invalidatedResult != result) {
122+
invalidateDependentInvocations(typeFlowAnalysis.workList);
123+
124+
invalidationCounter++;
125+
Statistics.maxInvalidationsPerInvocation =
126+
max(Statistics.maxInvalidationsPerInvocation, invalidationCounter);
127+
// In rare cases, loops in dependencies and approximation of
128+
// recursive invocations may cause infinite bouncing of result
129+
// types. To prevent infinite looping and guarantee convergence of
130+
// the analysis, result is saturated after invocation is invalidated
131+
// at least [_Invocation.invalidationLimit] times.
132+
if (invalidationCounter > _Invocation.invalidationLimit) {
133+
result =
134+
result.union(invalidatedResult, typeFlowAnalysis.hierarchyCache);
135+
}
136+
}
137+
invalidatedResult = null;
138+
}
139+
}
140+
111141
// Only take selector and args into account as _Invocation objects
112142
// are cached in _InvocationsCache using selector and args as a key.
113143
@override
@@ -261,7 +291,7 @@ class _DirectInvocation extends _Invocation {
261291
if (summaryResult is Type &&
262292
!typeFlowAnalysis.workList._isPending(this)) {
263293
assertx(result == null || result == summaryResult);
264-
result = summaryResult;
294+
setResult(typeFlowAnalysis, summaryResult);
265295
}
266296
return summary.apply(
267297
args, typeFlowAnalysis.hierarchyCache, typeFlowAnalysis);
@@ -1220,6 +1250,7 @@ class _WorkList {
12201250
void invalidateInvocation(_Invocation invocation) {
12211251
Statistics.invocationsInvalidated++;
12221252
if (invocation.result != null) {
1253+
assertx(invocation.invalidatedResult == null);
12231254
invocation.invalidatedResult = invocation.result;
12241255
invocation.result = null;
12251256
}
@@ -1269,45 +1300,62 @@ class _WorkList {
12691300

12701301
// Test if tracing is enabled to avoid expensive message formatting.
12711302
if (kPrintTrace) {
1272-
tracePrint('PROCESSING $invocation', 1);
1303+
tracePrint(
1304+
'PROCESSING $invocation, invalidatedResult ${invocation.invalidatedResult}',
1305+
1);
12731306
}
12741307

12751308
if (processing.add(invocation)) {
1309+
// Do not process too many calls in the call stack as
1310+
// it may cause stack overflow in the analysis.
1311+
const int kMaxCallsInCallStack = 500;
1312+
if (callStack.length > kMaxCallsInCallStack) {
1313+
Statistics.deepInvocationsDeferred++;
1314+
// If there is invalidatedResult, then use it.
1315+
// When actual result is inferred it will be compared against
1316+
// invalidatedResult and all dependent invocations will be invalidated
1317+
// accordingly.
1318+
//
1319+
// Otherwise, if invocation is not invalidated yet, use empty type
1320+
// as a result but immediately invalidate it in order to recompute.
1321+
// Static type would be too inaccurate.
1322+
if (invocation.invalidatedResult == null) {
1323+
invocation.result = const EmptyType();
1324+
}
1325+
// Conservatively assume that this invocation may trigger
1326+
// parameter type checks. This is needed because caller may not be
1327+
// invalidated and recomputed if this invocation yields the
1328+
// same result.
1329+
invocation.typeChecksNeeded = true;
1330+
invalidateInvocation(invocation);
1331+
assertx(invocation.result == null);
1332+
assertx(invocation.invalidatedResult != null);
1333+
assertx(_isPending(invocation));
1334+
if (kPrintTrace) {
1335+
tracePrint("Processing deferred due to deep call stack.");
1336+
tracePrint(
1337+
'END PROCESSING $invocation, RESULT ${invocation.invalidatedResult}',
1338+
-1);
1339+
}
1340+
processing.remove(invocation);
1341+
return invocation.invalidatedResult;
1342+
}
1343+
12761344
callStack.add(invocation);
12771345
pending.remove(invocation);
12781346

12791347
Type result = invocation.process(_typeFlowAnalysis);
12801348

1281-
assertx(result != null);
1282-
invocation.result = result;
1283-
1284-
if (invocation.invalidatedResult != null) {
1285-
if (invocation.invalidatedResult != result) {
1286-
invocation.invalidateDependentInvocations(this);
1287-
1288-
invocation.invalidationCounter++;
1289-
Statistics.maxInvalidationsPerInvocation = max(
1290-
Statistics.maxInvalidationsPerInvocation,
1291-
invocation.invalidationCounter);
1292-
// In rare cases, loops in dependencies and approximation of
1293-
// recursive invocations may cause infinite bouncing of result
1294-
// types. To prevent infinite looping and guarantee convergence of
1295-
// the analysis, result is saturated after invocation is invalidated
1296-
// at least [_Invocation.invalidationLimit] times.
1297-
if (invocation.invalidationCounter > _Invocation.invalidationLimit) {
1298-
result = result.union(
1299-
invocation.invalidatedResult, _typeFlowAnalysis.hierarchyCache);
1300-
invocation.result = result;
1301-
}
1302-
}
1303-
invocation.invalidatedResult = null;
1304-
}
1349+
invocation.setResult(_typeFlowAnalysis, result);
1350+
1351+
// setResult may saturate result to ensure convergence.
1352+
result = invocation.result;
13051353

13061354
// Invocation is still pending - it was invalidated while being processed.
13071355
// Move result to invalidatedResult.
13081356
if (_isPending(invocation)) {
13091357
Statistics.invocationsInvalidatedDuringProcessing++;
1310-
invocation.invalidatedResult = result;
1358+
invocation.invalidatedResult = invocation.result;
13111359
invocation.result = null;
13121360
}
13131361

pkg/vm/lib/transformations/type_flow/utils.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ class Statistics {
148148
static int maxInvocationsCachedPerSelector = 0;
149149
static int approximateInvocationsCreated = 0;
150150
static int approximateInvocationsUsed = 0;
151+
static int deepInvocationsDeferred = 0;
151152
static int classesDropped = 0;
152153
static int membersDropped = 0;
153154
static int methodBodiesDropped = 0;
@@ -177,6 +178,7 @@ class Statistics {
177178
maxInvocationsCachedPerSelector = 0;
178179
approximateInvocationsCreated = 0;
179180
approximateInvocationsUsed = 0;
181+
deepInvocationsDeferred = 0;
180182
classesDropped = 0;
181183
membersDropped = 0;
182184
methodBodiesDropped = 0;
@@ -207,6 +209,7 @@ class Statistics {
207209
${maxInvocationsCachedPerSelector} maximum invocations cached per selector
208210
${approximateInvocationsCreated} approximate invocations created
209211
${approximateInvocationsUsed} times approximate invocation is used
212+
${deepInvocationsDeferred} times invocation processing was deferred due to deep call stack
210213
${classesDropped} classes dropped
211214
${membersDropped} members dropped
212215
${methodBodiesDropped} method bodies dropped

0 commit comments

Comments
 (0)