Skip to content
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

Trimmer doesn't warn on reflection access to RUC override method #86008

Closed
vitek-karas opened this issue May 9, 2023 · 7 comments
Closed

Trimmer doesn't warn on reflection access to RUC override method #86008

vitek-karas opened this issue May 9, 2023 · 7 comments
Labels
area-Tools-ILLink .NET linker development as well as trimming analyzers
Milestone

Comments

@vitek-karas
Copy link
Member

Found by @MichalStrehovsky

interface IBase
{
    [RequriesUnreferencedCode("")]
    void DoSomethingDangerous();
}
class Implementation : IBase
{
    [RequiresUnreferencedCode("")]
    public void DoSomethingDangerous() { Console.WriteLine("!!!DANGER!!!"); }
}
void CallMethodOnInstance<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(T instance)
{
    typeof(T).GetMethod("DoSomethingDangerous").Invoke(instance, Array.Empty<object>());
}
static void Main()
{
    CallMethodOnInstance(new Implementation);
}

The above will not produce any warning when trimmed, but when executed it will print out !!!DANGER!!!.
The bug is that ILLink will not warn if a method is an override and is in RUC scope and is marked due to annotation (so broader marking like PublicMethods).
This was done assuming that the same broad marking will also mark the base method which will warn. But that's not always true. Reflection doesn't return base interface methods when usual GetMethod is called. ILLink doesn't enumerate the base interface methods in this case either.

@vitek-karas vitek-karas added the area-Tools-ILLink .NET linker development as well as trimming analyzers label May 9, 2023
@vitek-karas vitek-karas added this to the 8.0.0 milestone May 9, 2023
@ghost
Copy link

ghost commented May 9, 2023

Tagging subscribers to this area: @agocke, @sbomer, @vitek-karas
See info in area-owners.md if you want to be subscribed.

Issue Details

Found by @MichalStrehovsky

interface IBase
{
    [RequriesUnreferencedCode("")]
    void DoSomethingDangerous();
}
class Implementation : IBase
{
    [RequiresUnreferencedCode("")]
    public void DoSomethingDangerous() { Console.WriteLine("!!!DANGER!!!"); }
}
void CallMethodOnInstance<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(T instance)
{
    typeof(T).GetMethod("DoSomethingDangerous").Invoke(instance, Array.Empty<object>());
}
static void Main()
{
    CallMethodOnInstance(new Implementation);
}

The above will not produce any warning when trimmed, but when executed it will print out !!!DANGER!!!.
The bug is that ILLink will not warn if a method is an override and is in RUC scope and is marked due to annotation (so broader marking like PublicMethods).
This was done assuming that the same broad marking will also mark the base method which will warn. But that's not always true. Reflection doesn't return base interface methods when usual GetMethod is called. ILLink doesn't enumerate the base interface methods in this case either.

Author: vitek-karas
Assignees: -
Labels:

area-Tools-ILLink

Milestone: 8.0.0

@vitek-karas
Copy link
Member Author

There's an interesting discrepancy in behavior:

TestDirectReflectionAccess(new Derived());

// IL2026 - Base.DoSomethingDangerous
TestAnnotatedReflectionAccess(new Derived());

void TestDirectReflectionAccess(object instance)
{
    // IL2026 - Base.DoSomethingDangerous
    // IL2026 - Derived.DoSomethingDangerous
    typeof(Derived).GetMethod("DoSomethingDangerous")!.Invoke(instance, Array.Empty<object>());
}

void TestAnnotatedReflectionAccess<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(T instance)
{
    typeof(T).GetMethod("DoSomethingDangerous")!.Invoke(instance, Array.Empty<object>());
}

class Base
{
    [RequiresUnreferencedCode("Base")]
    public virtual void DoSomethingDangerous() { }
}

class Derived : Base
{
    [RequiresUnreferencedCode("Derived")]
    public override void DoSomethingDangerous() { }
}

If the reflection access is "direct", so the analysis can figure out the exact type and the method name, it will actually warn about both the derived and base methods. But if the access is indirect through annotation, then it only warns about the base method (the same logic as described in the original issue description kicks in an override methods do not warn).

@MichalStrehovsky
Copy link
Member

Would it be very bad if we just warn about all occurrences? Was the deduplication added based on negative feedback or are we just being proactive?

@vitek-karas
Copy link
Member Author

I'm still trying to figure out all the cases here, but in general I agree. My current working theory:

  • Warn on all occurrences - per the internal algorithm (whatever the algorithm finds, warn on it)
  • Don't warn when algorithm finds compiler generated code - this includes local functions and lambdas and obviously all of state machines, but will also trigger for things like delegate cache types for example.

There's another interesting thing I found:
In the above Base/Derived sample calling typeof(Derived).GetMethods() will return only Derived.DoSomethingDangerous, it will not return Base.DoSomethingDangerous. Similarly typeof(Derived).GetMethod() returns Derived.DoSomethingDangerous and does not fail since it doesn't "find" the base method. ILLink will see both methods in both cases.
Fixing this would require us to perform virtual override resolution - similar to what ILLink does today but used the other way round (avoid warning on base instead of on override).

I'm not sure we should try to do that - it's complex, linker will get it wrong (due to its buggy override resolution algo) and the value is questionable.

@vitek-karas vitek-karas changed the title Trimmer doesn't warn on reflection access to RUC over Trimmer doesn't warn on reflection access to RUC override method May 10, 2023
@vitek-karas
Copy link
Member Author

Another interesting discrepancy. Analyzer doesn't implement any of the above mentioned logic in ILLink and so it will actually generate 2 warnings in the DAM case (for base and derived).

@vitek-karas
Copy link
Member Author

Also related #86032

vitek-karas added a commit that referenced this issue May 12, 2023
These are tests for bug #86008.

I also restructured the test file into nested classes some more as it's easier to navigate this way.

Also adds a test for #86032.
vitek-karas added a commit that referenced this issue Jun 3, 2023
This modifies the behavior of both the illink and ilc to match and to implement some changes we've discussed recently - #86008.

The semantic changes (mostly as compared to previous illink behavior):
* Reflection access to a RUC virtual method will produce warning on all methods which have RUC, regardless of their relationships. Specifically if both the override and base methods are accessed, and they both have RUC, they will both generate a warning. (This effectively removes an optimization which illink had to avoid "duplicate" warnings where it tried to warn only on the base methods in this case. But this has holes - see #86008).
* Reflection access to compiler generated code will not produce warnings even if the target has RUC on it. It proved to be really difficult to design a good experience for reporting warnings in these cases. The problem is that developer has little control over the generated code and fully reporting all warnings leads to lot of noise. The above optimization in the illink tried to solve some of this, but it's not very successful. So we're removing all of these warnings. The reasoning is that reflection access to compiler generated members is an undefined behavior (even on non-trimmed, normal CLR) - and is likely to break for example between compiler versions - and there's no diagnostics about it ignoring trimming. Trimming/AOT just makes it a little bit worse.
* AOT compiler will not implement warnings caused by reflection access to compiler generated code which has annotations either (so `IL2118`, `IL2119` and `IL2120`). The plan is to eventually remove these from illink as well.

Note that there are exceptions to the above rules, which can be described as: Accessing a token of a member in IL is considered a reflection access (because the token can be turned into a reflection object), but it is also considered a direct reference. So in that case marking is implemented as "reflection access", but diagnostics is implemented as "direct reference". What this means is that accessing a compiler generated member through its token will still produce all warnings (RUC or DAM) as for non-compiler-generated member. This is important for things like creation of `Action` from a lambda, where the lambda is a compiler generated method, but we need to produce warnings if it uses annotations for example.

Refactorings:
* Refactored code in MarkStep to move most of the diagnostics logic into `ReportWarningsForReflectionAccess` - this is to mirror the design in ilc and also to make it more in sync with the already existing `ReportWarningsForTypeHierarchyReflectionAccess`.

Test changes:
* Adapting existing tests to the new behavior
* Adding some new tests, specifically around reflection access to compiler generated code and token access to compiler generated code.
* Enables `CompilerGeneratedCodeAccessedViaReflection` for ilc
@vitek-karas
Copy link
Member Author

This has been fixed in #86816.

The only piece remaining is to remove access-to-compiler-generated-code warnings from ILLink - so IL2118, IL2119 and IL2120. That work is tracked by #85042.

@dotnet dotnet locked as resolved and limited conversation to collaborators Jul 5, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-Tools-ILLink .NET linker development as well as trimming analyzers
Projects
None yet
Development

No branches or pull requests

2 participants