-
Notifications
You must be signed in to change notification settings - Fork 5.3k
ILLink: Fix instantiation tracking bug with [Intrinsic] methods.
#113434
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
ILLink: Fix instantiation tracking bug with [Intrinsic] methods.
#113434
Conversation
|
@vitek-karas Here is the intrinsic change we discussed on discord |
|
Tagging subscribers to this area: @dotnet/illink |
|
It does not look right to me to mark the type as instantiated just because it shows up as a return type of an intrinsic. Should the
The correctness of this and similar patterns in CoreCLR CoreLib is guaranteed by rooting all types in https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/corelib.h . For example, this line https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/corelib.h#L160 guarantees that RuntimeType is considered instantiated by the trimmer. |
Along the lines of dotnet#113434. The existing `ProcessInteropMethod` has a bug where the return type and/or out parameter types will not be marked as instantiated. In this case, the bug happens when the return type or parameter types do not have a `.ctor()`. This is because `ProcessInteropMethod` will call `MarkDefaultConstructor` on the type which will cause the type to be marked instantiated if the `.ctor()` exists.
I found it while getting the linker tests running on a mono based .NET 8 BCL build. The following test code
```
public static void Main ()
{
Helper(null);
}
[Kept]
static void Helper (object[] maybetypes)
{
Type[] types = new Type[] { maybetypes[0] as Type };
}
```
Will have `Helper` modified to be essentially
```
Type[] types = new Type[] { null };
```
What's happening is, `System.Type` is not marked as instantied which leads to `ProcessPendingTypeChecks` kicking in. This problem doesn't happen with a normal CoreCLR .NET 8 BCL. Here's why.
With CoreCLR, `System.Object` has
```
[MethodImpl(MethodImplOptions.InternalCall)]
[Intrinsic]
public extern Type GetType();
```
The icall flag leads to `ProcessInteropMethod` being called. Which will then mark the default constructor of the return type, which then leads to `System.Type` being marked as instantiated.
With the mono based BCL, `GetType` looks like
```
[Intrinsic]
public Type GetType()
{
return GetType();
}
```
The fix is to have the linker record the return type and any out parameters as instantiated.
07c0f81 to
860606b
Compare
|
@jkotas A little more context here https://discord.com/channels/732297728826277939/751137004007456849/1349389472545898497
Yes it could and that would fix this issue as well.
What is your concern with this approach? It changes the handling of 1 method across the CoreCLR BCL and Mono BCL. What is the value in not doing this? Using DynamicDependencyAttribute to mark the ctor will lead to a mark as instantiated anyways. An instance of the returned type can be created without invoking an instance constructor, this approach covers that scenario. I don't think the instantiation optimization is worth attempting to retain on types that could (and probably are in most cases) created on the native side. Finding this bug was incredibly obscure. I got lucky that an existing test combine with the mono bcl assemblies caught it. Maybe if the trimming analyzer issued warnings on all |
Along the lines of dotnet#113434. The existing `ProcessInteropMethod` has a bug where the return type and/or out parameter types will not be marked as instantiated. In this case, the bug happens when the return type or parameter types do not have a `.ctor()`. This is because `ProcessInteropMethod` will call `MarkDefaultConstructor` on the type which will cause the type to be marked instantiated if the `.ctor()` exists.
|
Here's a PR for a similar issue #113437 |
|
|
|
Vast majority of [Intrinsic] methods do not have any extra type decencies. I do not think this would help since people would just copy&paste the boiler plate dummy dependency everywhere, and the few cases that need it would miss it anyway. A correct by construction solution would be to require references to types referenced by the VM to be captured on the managed side in some form and stop calling |
|
@jkotas @marek-safar What are your thoughts on #113437 ? Should that change happen? |
|
Here's some data. In the 2620 of those have a value type as the result return type. ValueTypes are always marked as instantiated so this PR would have zero impact on those methods. That leaves 13 methods that could see a behavior change from this PR. Of those 13 methods, 4 are returning Now we are down to 9 methods. The next return type is Now we are down to 7 methods. The remaining methods are Why is it invalid for the linker to assume an instance of these return types could exist when these 7 methods are used? I see the argument for adding I'm struggling to understand why the not instantiated optimization is worth trying to maintain for methods that will execute code that is different from the IL that the linker is able to see. Incorrectly applying the not instantiated optimizations does not lead to simple runtime problems like MissingMethodExceptions. Incorrectly applying the not instantiated optimizations can lead to obscure runtime behavior changes. I don't think it's worth the risk to apply not instantiated optimizations to types coming out of methods to which the linker does not have visibility into the actual method body. |
Right, 6 out of 7 methods in your list return more specialized type instance and this PR is not sufficient to make them correct. |
Along the lines of dotnet#113434. The existing `ProcessInteropMethod` has a bug where the return type and/or out parameter types will not be marked as instantiated. In this case, the bug happens when the return type or parameter types do not have a `.ctor()`. This is because `ProcessInteropMethod` will call `MarkDefaultConstructor` on the type which will cause the type to be marked instantiated if the `.ctor()` exists.
You are correct. This PR will not make them correct. I will open a separate PR to add the DynamicDependencyAttribute. What this PR does is reduce the risk of missed dependencies causing obscure problems. And slightly unifies intrinsics and icalls/pinvoke so that there is less of a behavior difference. |
Don't we want this to cause problems? That is, it is a top-level goal for trimming to not "over-approximate" the app in order to make things work. That doesn't result in long-term customer success, it just hides bugs. It seems like the aggressive trimming in this case has actually caught a bug that will now be fixed, and everything will be happy. |
@agocke I want consistency more than anything between the two variations of
This is a niche corner behavior where some precautions are already taken. For example here and here. Now that I look at You are correct, that is normally the rational that is followed. And we could follow that here. If we did, I'd argue we should also remove the MarkDefaultConstructor calls in my other PR. I didn't propose that as a solution because I wasn't sure what the appetite would be for changing an existing behavior. Would you agree that |
|
Status update. Keep in mind I'm using UnityLinker + net7 ILLink (from the
Which is doing The net7 illink code doesn't have this change. So this problem was encountered before and the test was adjusted to make the failure go away.
However, the following does fix the problem.
The findings from (2) and (3) lead me to believe that just because a derived type is marked as instantiated doesn't mean the base type will be. That seems like a bug if true. I'll put together a test to more explicitly verify whether or not that is happening. |
|
I'm going to close this PR. I will address #113437 (comment) first. I will circle back later and try to remove |
I found it while getting the linker tests running on a mono based .NET 8 BCL build. The following test code
Will have
Helpermodified to be essentiallyWhat's happening is,
System.Typeis not marked as instantied which leads toProcessPendingTypeCheckskicking in. This problem doesn't happen with a normal CoreCLR .NET 8 BCL. Here's why. With CoreCLR,System.ObjecthasThe icall flag leads to
ProcessInteropMethodbeing called. Which will then mark the default constructor of the return type, which then leads toSystem.Typebeing marked as instantiated. With the mono based BCL,GetTypelooks likeThe fix is to have the linker record the return type and any out parameters as instantiated.