-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Check ref safety of arg mixing in collection initializers #76237
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
Changes from 2 commits
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -4455,11 +4455,12 @@ internal SafeContext GetValEscape(BoundExpression expr, SafeContext scopeOfTheCo | |||||
|
|
||||||
| case BoundKind.CollectionInitializerExpression: | ||||||
| var colExpr = (BoundCollectionInitializerExpression)expr; | ||||||
| return GetValEscape(colExpr.Initializers, scopeOfTheContainingExpression); | ||||||
|
|
||||||
| case BoundKind.CollectionElementInitializer: | ||||||
| var colElement = (BoundCollectionElementInitializer)expr; | ||||||
| return GetValEscape(colElement.Arguments, scopeOfTheContainingExpression); | ||||||
| // If arg mixing fails when the receiver has calling-method scope (i.e., some arguments could escape into the receiver), make the value scoped. | ||||||
| return | ||||||
| !scopeOfTheContainingExpression.IsConvertibleTo(SafeContext.CallingMethod) && | ||||||
| !CheckValEscapeOfCollectionInitializer(colExpr, escapeFrom: scopeOfTheContainingExpression, escapeTo: SafeContext.CallingMethod, BindingDiagnosticBag.Discarded) | ||||||
| ? scopeOfTheContainingExpression | ||||||
| : SafeContext.CallingMethod; | ||||||
|
|
||||||
| case BoundKind.ObjectInitializerMember: | ||||||
| // this node generally makes no sense outside of the context of containing initializer | ||||||
|
|
@@ -4468,11 +4469,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 +4782,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); | ||||||
|
Contributor
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.
Member
Author
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 +5277,9 @@ 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); | ||||||
|
|
||||||
| // 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 CheckValEscapeOfCollectionInitializer(colExpr, escapeFrom, escapeTo, diagnostics); | ||||||
|
|
||||||
| case BoundKind.PointerElementAccess: | ||||||
| var accessedExpression = ((BoundPointerElementAccess)expr).Expression; | ||||||
|
|
@@ -5561,21 +5573,53 @@ private bool CheckValEscapeOfObjectInitializer(BoundObjectInitializerExpression | |||||
| return true; | ||||||
| } | ||||||
|
|
||||||
| #nullable disable | ||||||
|
|
||||||
| private bool CheckValEscape(ImmutableArray<BoundExpression> expressions, SafeContext escapeFrom, SafeContext escapeTo, BindingDiagnosticBag diagnostics) | ||||||
| private bool CheckValEscapeOfCollectionInitializer(BoundCollectionInitializerExpression colExpr, SafeContext escapeFrom, SafeContext escapeTo, BindingDiagnosticBag diagnostics) | ||||||
| { | ||||||
| foreach (var expression in expressions) | ||||||
| // 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. | ||||||
|
Member
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.
Suggested change
suggest using exactly the same name here to make it just a little bit easier to grasp what this comment is conveying. |
||||||
|
|
||||||
| bool result = true; | ||||||
| using var _ = new PlaceholderRegion(this, [(colExpr.Placeholder, escapeTo)]) { ForceRemoveOnDispose = true }; | ||||||
| foreach (var initializer in colExpr.Initializers) | ||||||
| { | ||||||
| if (!CheckValEscape(expression.Syntax, expression, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics)) | ||||||
| switch (initializer) | ||||||
| { | ||||||
| return false; | ||||||
| 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; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return true; | ||||||
| return result; | ||||||
| } | ||||||
|
|
||||||
| #nullable disable | ||||||
|
|
||||||
| private bool CheckInterpolatedStringHandlerConversionEscape(BoundExpression expression, SafeContext escapeFrom, SafeContext escapeTo, BindingDiagnosticBag diagnostics) | ||||||
| { | ||||||
| var data = expression.GetInterpolatedStringHandlerData(); | ||||||
|
|
||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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.
Why should we predicate this only on convertibility to
CallingMethodscope? Consider the following:In this case,
scopeOfTheContainingExpressionisReturnOnly(right?), and not convertible toCallingMethod. But don't we still need to do a mixing check here?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.
I could also imagine scenarios where
new RS(...)has some local lifetime, but a collection initializer argument has an even narrower lifetime.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.
The check
!scopeOfTheContainingExpression.IsConvertibleTo(SafeContext.CallingMethod)is just an optimization - if it's false, it actually meansscopeOfTheContainingExpression == SafeContext., and we can returnSafeContext.CallingMethodimmediately.I will refactor the code to make that clearer.
We do a mixing check there. The
!scopeOfTheContainingExpression.IsConvertibleTo(SafeContext.CallingMethod)condition istruein that case.However, I'm not sure how that example is different from currently tested scenarios - you added a
ref paramto the ref struct's constructor, but that already goes through different code paths, in this PR I've only changed handling of the collection initializer, i.e., the part after the constructor -{ local }.I don't follow. Here we are computing ValEscape for the expression
new R() { ... }, so it does not yet have any lifetime. And we either determine the expression to have ValEscape ofscopeOfTheContainingExpressionorCallingMethod. I don't think the collection initializer argument can have narrower lifetime thanscopeOfTheContainingExpression(that's the narrowest lifetime we are currently at).