-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Check ref safety of arg mixing in collection initializers #76237
base: main
Are you sure you want to change the base?
Changes from 1 commit
e9d7a35
333529c
55fb093
a80efd9
461ab15
f350350
7dbbe97
601b822
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4454,12 +4454,52 @@ internal SafeContext GetValEscape(BoundExpression expr, SafeContext scopeOfTheCo | |
return GetValEscapeOfObjectInitializer(initExpr, scopeOfTheContainingExpression); | ||
|
||
case BoundKind.CollectionInitializerExpression: | ||
var colExpr = (BoundCollectionInitializerExpression)expr; | ||
return GetValEscape(colExpr.Initializers, scopeOfTheContainingExpression); | ||
{ | ||
var colExpr = (BoundCollectionInitializerExpression)expr; | ||
|
||
// var c = new C() { element }; | ||
// | ||
// is equivalent to | ||
// | ||
// var c = new C(); | ||
// c.Add(element); | ||
// | ||
// So we check arg mixing of `(receiverPlaceholder).Add(element)` calls, | ||
// and make the result "scoped" if any call could cause the elements to escape. | ||
|
||
using var _ = new PlaceholderRegion(this, [(colExpr.Placeholder, SafeContext.CallingMethod)]) { ForceRemoveOnDispose = true }; | ||
foreach (var initializer in colExpr.Initializers) | ||
{ | ||
switch (initializer) | ||
{ | ||
case BoundCollectionElementInitializer init: | ||
if (!CheckInvocationArgMixing( | ||
init.Syntax, | ||
MethodInfo.Create(init.AddMethod), | ||
receiverOpt: colExpr.Placeholder, | ||
receiverIsSubjectToCloning: init.InitialBindingReceiverIsSubjectToCloning, | ||
parameters: init.AddMethod.Parameters, | ||
argsOpt: init.Arguments, | ||
argRefKindsOpt: default, | ||
argsToParamsOpt: init.ArgsToParamsOpt, | ||
scopeOfTheContainingExpression: scopeOfTheContainingExpression, | ||
BindingDiagnosticBag.Discarded)) | ||
{ | ||
return scopeOfTheContainingExpression; | ||
} | ||
break; | ||
|
||
case BoundDynamicCollectionElementInitializer dynamicInit: | ||
break; | ||
|
||
default: | ||
Debug.Fail($"{initializer.Kind} expression of {initializer.Type} type"); | ||
break; | ||
} | ||
} | ||
|
||
case BoundKind.CollectionElementInitializer: | ||
var colElement = (BoundCollectionElementInitializer)expr; | ||
return GetValEscape(colElement.Arguments, scopeOfTheContainingExpression); | ||
return SafeContext.CallingMethod; | ||
} | ||
|
||
case BoundKind.ObjectInitializerMember: | ||
// this node generally makes no sense outside of the context of containing initializer | ||
|
@@ -4468,11 +4508,18 @@ internal SafeContext GetValEscape(BoundExpression expr, SafeContext scopeOfTheCo | |
return scopeOfTheContainingExpression; | ||
|
||
case BoundKind.ImplicitReceiver: | ||
case BoundKind.ObjectOrCollectionValuePlaceholder: | ||
// binder uses this as a placeholder when binding members inside an object initializer | ||
// just say it does not escape anywhere, so that we do not get false errors. | ||
return scopeOfTheContainingExpression; | ||
|
||
case BoundKind.ObjectOrCollectionValuePlaceholder: | ||
if (_placeholderScopes?.TryGetValue((BoundObjectOrCollectionValuePlaceholder)expr, out var scope) == true) | ||
{ | ||
return scope; | ||
} | ||
|
||
return scopeOfTheContainingExpression; | ||
|
||
case BoundKind.InterpolatedStringHandlerPlaceholder: | ||
// The handler placeholder cannot escape out of the current expression, as it's a compiler-synthesized | ||
// location. | ||
|
@@ -4774,6 +4821,18 @@ internal bool CheckValEscape(SyntaxNode node, BoundExpression expr, SafeContext | |
} | ||
return true; | ||
|
||
case BoundKind.ObjectOrCollectionValuePlaceholder: | ||
if (_placeholderScopes?.TryGetValue((BoundObjectOrCollectionValuePlaceholder)expr, out var scope) == true) | ||
{ | ||
if (!scope.IsConvertibleTo(escapeTo)) | ||
{ | ||
Error(diagnostics, inUnsafeRegion ? ErrorCode.WRN_EscapeVariable : ErrorCode.ERR_EscapeVariable, node, expr.Syntax); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
return inUnsafeRegion; | ||
} | ||
return true; | ||
} | ||
goto default; | ||
|
||
case BoundKind.Local: | ||
var localSymbol = ((BoundLocal)expr).LocalSymbol; | ||
if (!GetLocalScopes(localSymbol).ValEscapeScope.IsConvertibleTo(escapeTo)) | ||
|
@@ -5257,17 +5316,52 @@ internal bool CheckValEscape(SyntaxNode node, BoundExpression expr, SafeContext | |
var initExpr = (BoundObjectInitializerExpression)expr; | ||
return CheckValEscapeOfObjectInitializer(initExpr, escapeFrom, escapeTo, diagnostics); | ||
|
||
// this would be correct implementation for CollectionInitializerExpression | ||
// however it is unclear if it is reachable since the initialized type must implement IEnumerable | ||
case BoundKind.CollectionInitializerExpression: | ||
var colExpr = (BoundCollectionInitializerExpression)expr; | ||
return CheckValEscape(colExpr.Initializers, escapeFrom, escapeTo, diagnostics); | ||
{ | ||
var colExpr = (BoundCollectionInitializerExpression)expr; | ||
|
||
// return new C() { element }; | ||
// | ||
// is equivalent to | ||
// | ||
// var c = new C(); | ||
// c.Add(element); | ||
// return c; | ||
// | ||
// So we check arg mixing of `(receiverPlaceholder).Add(element)` calls | ||
// where the placeholder has `escapeTo` scope and the call has `escapeFrom` scope. | ||
|
||
bool result = true; | ||
using var _ = new PlaceholderRegion(this, [(colExpr.Placeholder, escapeTo)]) { ForceRemoveOnDispose = true }; | ||
foreach (var initializer in colExpr.Initializers) | ||
{ | ||
switch (initializer) | ||
{ | ||
case BoundCollectionElementInitializer init: | ||
result |= CheckInvocationArgMixing( | ||
init.Syntax, | ||
MethodInfo.Create(init.AddMethod), | ||
receiverOpt: colExpr.Placeholder, | ||
receiverIsSubjectToCloning: init.InitialBindingReceiverIsSubjectToCloning, | ||
parameters: init.AddMethod.Parameters, | ||
argsOpt: init.Arguments, | ||
argRefKindsOpt: default, | ||
argsToParamsOpt: init.ArgsToParamsOpt, | ||
scopeOfTheContainingExpression: escapeFrom, | ||
diagnostics); | ||
break; | ||
|
||
case BoundDynamicCollectionElementInitializer dynamicInit: | ||
break; | ||
|
||
default: | ||
Debug.Fail($"{initializer.Kind} expression of {initializer.Type} type"); | ||
break; | ||
} | ||
} | ||
|
||
// this would be correct implementation for CollectionElementInitializer | ||
// however it is unclear if it is reachable since the initialized type must implement IEnumerable | ||
case BoundKind.CollectionElementInitializer: | ||
var colElement = (BoundCollectionElementInitializer)expr; | ||
return CheckValEscape(colElement.Arguments, escapeFrom, escapeTo, diagnostics); | ||
return result; | ||
} | ||
|
||
case BoundKind.PointerElementAccess: | ||
var accessedExpression = ((BoundPointerElementAccess)expr).Expression; | ||
|
@@ -5563,19 +5657,6 @@ private bool CheckValEscapeOfObjectInitializer(BoundObjectInitializerExpression | |
|
||
#nullable disable | ||
|
||
private bool CheckValEscape(ImmutableArray<BoundExpression> expressions, SafeContext escapeFrom, SafeContext escapeTo, BindingDiagnosticBag diagnostics) | ||
{ | ||
foreach (var expression in expressions) | ||
{ | ||
if (!CheckValEscape(expression.Syntax, expression, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics)) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private bool CheckInterpolatedStringHandlerConversionEscape(BoundExpression expression, SafeContext escapeFrom, SafeContext escapeTo, BindingDiagnosticBag diagnostics) | ||
{ | ||
var data = expression.GetInterpolatedStringHandlerData(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
ForceRemoveOnDispose = true
because otherwise it can happen that the placeholder is added twice causing an assert to fail.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might need to be extended to also handle the assert in the Add part, like I'm doing in a similar PR (#76263): b64ee87
But the bigger question is whether this looks like a good approach in general - using PlaceholderRegion temporarily during the check, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like adding a PlaceholderRegion temporarily here works, and it is unfortunate that PlaceholderRegions are typically added during the Visit methods and not removed. I'm curious though: was there a reason it was necessary to add the PlaceholderRegion temporarily in this particular case rather than adding it once (and not removing), in the calling
VisitObjectCreationExpressionBase()
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. When GetValEscape is called on a collection initializer, I need to decide whether it can be returnable or must be scoped to current block. So I temporarily mark the receiver with CallingMethod scope to see if that is ref safe - but it might not be ref safe (and depending on that, I determine the ValEscape of the collection initializer). Put another way, the receiver scope is just speculative and hence I don't think it should be set permanently in the visitor.