-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
ResourceManager contains code which ILLink can't analyze #32862
Comments
In order to fix this, I think we will need to teach ILLink how to read Using reflection here is a key to the design of System.Resources.Extensions, which is used by WinForms/WPF when putting Images and other types as resources. (Note, it can be used by any .NET Core app, it isn't limited to WinForms/WPF.) The I'm not sure how much work it will be to teach ILLink how to read Using reflection to create
|
private static readonly Type s_xDocument = Type.GetType("System.Xml.Linq.XDocument, System.Private.Xml.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51")!; |
It'd need to understand the formats used by ResourceReader as well as DeserializingResourceReader. Both formats can represent types in resources. Here's a sample I wrote a while back to demonstrate reading the types out of a ResourceReader stream: https://gist.github.com/ericstj/b0e8537f29dabdd73f726e8fee0f49d6 For ResourceReader & DeserializingResourceReader (binary format case) it would need to root the type, and whatever serialization constructor it had, as well as BinaryFormatter. For DeserializingResourceReader there are also cases where we'd need to root the TypeConverter. FWIW I expect this problem to be similar to Baml, though Baml has a much more complex format along with hundreds of special cases |
Note that the BinarySerializer path is only needed by legacy workloads (e.g. WinForms). It should not be needed by modern workloads (e.g. Blazor) unless they take dependency on a legacy component. We should have a switch to disable use of BinarySerializer. This will solve the binary serializer problem for both ResourceReader and everything else going forward. This switch should be turned on by default (ie even without IL linker) for all modern workloads that are not expected to need BinarySerializer. This switch is goodness for security hardening against binary serializer attacks (cc @GrabYourPitchforks). I expect that we would make it a best practice to turn this on for cloud services. The work you are describing above is only needed for legacy workloads that need BinarySerializer. I believe that these workloads end up being under the cut line for "works great with IL linker" for .NET 5. |
Which workloads would be above the cut line for "works great with IL linker" for .NET 5? I'm assuming Blazor and Xamarin are. What about web / micro services? How about WPF on .NET 5? Are we enabling developers building single file Windows applications to trim their application? For Blazor and Xamarin, my understanding is that those workloads don't use System.Resources.ResourceManager. |
Yes, this sounds about right. We are still working through the exact web / micro services to target.
They do use it for strings. Every other netcoreapp library that we ship uses ResourceManager for strings. |
The implementation of
ResourceManager
has a code paths that usesType.GetType
andActivator.CreateInstance
on types that are not known by the linker, effectively making allResourceManager.GetString
calls potentially linker-unsafe. This in turn means that applications using ILLink during publish may end up broken at runtime due to missing assemblies/types/members.Creation of
ResourceSet
andIResourceReader
ManifestBasedResourceGroveler.CreateResourceSet
reads strings from the resource stream to get the type names of a resource manager and resource set to create. If they are "System.Resources.ResourceReader" and "System.Resources.RuntimeResourceSet", which is likely the most common case by far, it simply creates aRuntimeResourceSet
without reflection. If not, then it will useType.GetType
to get the types specified by name in the stream, and callActivator.CreateInstance
on them. The resource reader instance gets cast toIResourceReader
, and the resource set instance gets cast toResourceSet
. For the resource set, it may also instead use a user-specifiedSystem.Type
that was passed to theResourceManager
constructor (userResourceSet
), which takes precedence over the type specified in the resource stream and get instantiated viaActivator.CreateInstance
.Without knowledge of the resource stream types, the linker can't tell if this is safe as the resource set or resource reader types may not be referenced anywhere else in the application's code. As such linker would tret them as dead code and remove them.
Creating instances of any type specified in the resource stream by its name
The default resource reading path will call
Type.GetType
on a type name retrieved from the resource stream, even for reading strings.The default path for
ResourceManager.GetString
orResourceManager.GetObject
(which usesRuntimeResourceSet
andResourceReader
) uses a shared helper,RuntimeResourceSet.GetObject
. This fans out into three different implementations: one for reading strings (ResourceReader.LoadString
), and two for objects (ResourceReader.LoadObject
usesLoadObjectV1
orLoadObjectV2
depending on the version of the resource stream). All of these call intoFindType
, which doesType.GetType
on a type name retrieved from the resource stream:LoadString
callsFindType
, and checks that the found type wastypeof(string)
before reading a string from the resource stream. This could possibly be factored so that theGetString
path does not useType.GetType
, but just checks the string in the stream. Reading strings is probably the most common use case.LoadObjectV1
similarly callsFindType
, and compares the found type to a few well-known types before falling back to the general case (DeserializeObject
)LoadObjectV2
has an optimization that first reads a type code from the resource stream, and compares it to well-known type codes for built-in types. These paths do not rely on reflection. Only if the type code is not a known type does it callDeserializeObject
, which in turn callsFindType
.Deserialization when reading resources
Reading objects from a resource stream relies on
BinaryFormatter.Deserialize
for all but a few special-cased types.BinaryFormatter
is lazily initialized using unsafe reflection.The
ResourceReader
constructor takes a parameter that can disable the deserialization path, but it looked to me like deserialization was enabled by default. I might be missing something here - I didn't check whetherBinaryFormatter
is somehow disabled in .NET Core, so maybe this is a light-up scenario.The deserialization process itself is not linker safe as it reads types names from the stream and creates instances of those objects. Also it will set values on properties/fields which may not be accessed otherwise and thus removed by the linker.
Using reflection to create
BinaryFormatter
Because the code is in CoreLib and the
BinaryFormatter
is implemented inSystem.Runtime.Serialization.Formatters
there's no direct compile time dependency. Instead theBinaryFormatter
is created via reflection inResourceReader.InitializeBinaryFormatter
. The way the code is written makes it not analyzable by linker as it won't see through storing values to static variables and closures. Overall this can probably be made linker-safe if property annotated withPreserveDependency
attributes.(Kudos to @sbomer for most of the analysis on this problem)
The text was updated successfully, but these errors were encountered: