-
Notifications
You must be signed in to change notification settings - Fork 126
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
Warn on mismatch for ref params #2769
Conversation
Not only do we need to tell whether a ref parameter comes from a local/field/parameter, we also need to track the side-effect of a local being assigned through a ref param. This feels like a concern which may be better handled by the part of the code that's already responsible for tracking locals/assignments, so the visitor on the analyzer side and the scanner on the linker side. On the analyzer side, I think this will show up as an assignment to a FlowCaptureReference, and we can tell from
Agreed, I would not handle this in |
Awesome write-up by the way! |
Doing this is not going to break anything, but it's not strictly speaking correct. After the call the caller has no idea what is the value of the ref - it is basically an out parameter at that point. It's true that some methods will not modify the value and so there's a possibility it will have the previous value, but there's no way to tell and more importantly there are methods which will always overwrite in which case it's actually wrong to remember the previous values. I think there might be ways to expose this in such a way that the code will warn in places where it should not (but maybe there's no such way). For ref parameters I think a good way to model them from a data flow point of view is that they're |
I must admit I don't understand why it has to be this way. Currently if you observe the behavior of the analysis from the outside (the developer of the app) there are no locals. All the warnings we report are in terms of explicitly annotatable entities (params, fields, methods, ...). This has a good reason behind it - the developer can't do anything about the local since it can't be annotated by hand. So in your example above: // Warns correctly
MethodWithRefParameter (ref s_typeWithPublicParameterlessConstructor);
// Local variable gets FieldValue
var x = s_typeWithPublicParameterlessConstructor;
// Warns because there is a FieldValue, but shouldn't because the variable referenced is a local which can't be annotated
MethodWithRefParameter (ref x); I think it's perfectly OK that we warn in both cases. In fact it would be a bug if we didn't warn in the second case. Take this for example: void PrintProperties([DAM(PublicProperties)] ref Type t) { ... t.GetProperties() ... }
[DAM(PublicMethods)] Type s_typeWithMethods;
void Test()
{
var type = s_typeWithMethods;
PrintProperties(ref type); // MUST WARN
} The call to |
I haven't spend much time thinking about this, but right now I think ref/out parameters are relatively the easier part. Ref return values are also interesting but not too much (they act a lot like out parameters in a way). Ref locals are a different matter and they will be tricky.
I agree, but maybe we can start simple and only solve parameters on their own for now as they're much more common. |
I don't understand the need for a I think this is one of the core problems to solve here:
I ran into it with the For the ref params maybe we will need to make the state dictionary mutable - I didn't look in detail what that would mean. |
Just realized I didn't annotate things right in my example. I agree with your example that both should warn. For the example I'm thinking of it would look like this: [DAM (DAMT.PublicFields | DAMT.PublicMethods)]
Type s_field;
void MethodWithRefParameter ([DAM (DAMT.PublicFields)] ref Type type) { }
// Warns correctly
MethodWithRefParameter (ref s_field);
// Local variable gets FieldValue
var x = s_field;
// Warns because there is a FieldValue, but shouldn't because the variable referenced is a local which can't be annotated
MethodWithRefParameter (ref x); Since the field is annotated with PublicMethods and PublicFields (and that value can't change), we cannot pass it as the ref parameter since the method could assign a value without PublicMethods to the field. However, the local |
My understanding is that For example, I think it would be less confusing to have a different warning message for this: [DAM (DAMT.PublicFields | DAMT.PublicMethods)]
Type s_field;
void MethodWithRefParameter ([DAM (DAMT.PublicFields)] out Type type) { }
void Method (Type type)
{
// Warning: by-reference parameter 'type' of method 'MethodWithRefParameter' does not meet DAM requirements of field 's_field'
MethodWithRefParameter (out s_field);
MethodWithRefParameter (out var x);
// Warning: by-reference parameter 'type' of method 'MethodWithRefParameter' does not meet DAM requirements of field 's_field'
s_field = x;
// Warning: parameter 'type' of method 'Method' does not meet DAM requirements of field 's_field'
s_field = type;
} I was thinking ByRefParameter would only be used as a source for the values passed to the calling method (never a target for a value) and checking that dataflow values match going from parameter -> argument. Then we would continue to use MethodParameterValue when checking dataflow from argument -> parameter. If we think it complicates things more than it's worth it isn't strictly necessary, though. |
Re field versus local: That's a good example, I didn't think of that. I think you're right that we will have to add something here. One thing we had previously but removed it for now is values which are "see through" for diagnostics. So basically something like a |
Re |
@vitek-karas @sbomer and I spoke today about the path forward for ref params and ref locals. The concept of references should be contained in as little code as possible (preferably only in MethodBodyScanner in linker and TrimAnalysisVisitor in analyzer) to avoid leaking into shared code and requiring special casing everywhere there. In the linker, we can create a new ReferenceValue with variants for locals, fields parameters, and array elements, that contains the info necessary to retrieve the value when needed. We should avoid having an actual reference to a location in memory because values are copied and Ref locals and more complex reference type / array value handling is not a likely scenario and should be a lower priority. For now we'll just solve ref parameters as simply as possible. Vitek and Sven, did I miss anything else from our discussion? |
I think that captures it! |
{ | ||
VariableDefinition localDef = GetLocalDef (operation, methodBody.Variables); | ||
if (localDef == null) { | ||
PushUnknownAndWarnAboutInvalidIL (currentStack, methodBody, operation.Offset); | ||
return; | ||
} | ||
|
||
bool isByRef = operation.OpCode.Code == Code.Ldloca || operation.OpCode.Code == Code.Ldloca_S | ||
|| localDef.VariableType.IsByRefOrPointer (); | ||
bool isByRef = operation.OpCode.Code == Code.Ldloca || operation.OpCode.Code == Code.Ldloca_S; |
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.
Could you add a comment that this doesn't yet support ref types (so a local ref variable for example)?
Since the code before the change sort of tried to support that.
Another interesting thing to add to notes/todo/issues - pointer types - I don't know how they should be handled - just that it's VERY low priority.
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 think this should still handle ref locals fine. It's just if VariableType.IsByRefOrPointer returns true, the value held should already be a ReferenceValue
, and we wouldn't want to wrap it in another LocalVariableReferenceValue
.
// after the call with out parameter. | ||
[ExpectedWarning ("IL2072", nameof (DataFlowTypeExtensions.RequiresPublicMethods))] | ||
[ExpectedWarning ("IL2072", nameof (DataFlowTypeExtensions.RequiresPublicMethods), ProducedBy = ProducedBy.Analyzer)] |
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.
Do we have an issue tracking the fact that analyzer doesn't seem to handle ref parameters correctly?
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.
We have https://github.com/dotnet/linker/issues/2632 which generally is about ref and out parameters, but I don't think we have one specifically for the analyzers.
Share switch when storing a value by ref Remove separate errors for by-ref parameters
- Warn on unhandled StoreReference - Update warning codes for reference tracking failures - Add HandleStoreThisParameter and HandleStoreReturnValue for assigning to refs of `this` or ref return values - Add ArrayElementReferenceValue and reset elements when a reference is taken.
test/Mono.Linker.Tests.Cases/DataFlow/MethodByRefParameterDataFlow.cs
Outdated
Show resolved
Hide resolved
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.
Looks good. Great work!
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.
LGTM, thanks a lot!
Commit migrated from dotnet/linker@87539d4
This is a first attempt at adding tracking for
ref
params in the linker.The expected behavior for by-reference parameters is:
ref
parametersref
should contain the result ofMeet
-ing the previous value with aByRefParameterValue
out
parametersout
parameter should only contain theByRefParameterValue
in
parametersThis change enforces DAM annotations match for
ref
values in all cases where theref
argument might be an annotated variable. However, this causes excess warnings when a local variable has aFieldValue
orMethodParameterValue
.This gets to the main issue / design decision for implementing by-reference parameters. There is no way to tell what kind of variable is passed as a
ref
parameter, but we want different behavior for locals (which cannot be annotated) and fields or parameters (which have immutable annotations). We also should keep in mind howref
locals would fit into the picture when we design this.One way we could go about this is adding a flag with what the source of the values in MultiValue. I think anytime we
Meet
values to create a new MultiValue, it must be placed in a local variable, where the implicit conversion from a SingleValue should mean that the value comes directly from that field / parameter etc. If we add a flag that gets set only if the MultiValue comes from aMeet
, we could differentiate between locals and directly annotated values. However, this means we'd have to enforce thatMeet
is used if and only if the target is a local variable, and it might be easy to accidentally assign a value to a local variable directly and lose that invariant.To update the values in the local state after the method call, we could flow the new values for the by-ref params back up to the local state and update the values there. However, it might make sense to make a type to make a representation of references that enable mutability of the referenced variable's value.