Skip to content

ResourceManager contains code which ILLink can't analyze #32862

@vitek-karas

Description

@vitek-karas

The implementation of ResourceManager has a code paths that uses Type.GetType and Activator.CreateInstance on types that are not known by the linker, effectively making all ResourceManager.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 and IResourceReader

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 a RuntimeResourceSet without reflection. If not, then it will use Type.GetType to get the types specified by name in the stream, and call Activator.CreateInstance on them. The resource reader instance gets cast to IResourceReader, and the resource set instance gets cast to ResourceSet. For the resource set, it may also instead use a user-specified System.Type that was passed to the ResourceManager constructor (userResourceSet), which takes precedence over the type specified in the resource stream and get instantiated via Activator.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 or ResourceManager.GetObject (which uses RuntimeResourceSet and ResourceReader) 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 uses LoadObjectV1 or LoadObjectV2 depending on the version of the resource stream). All of these call into FindType, which does Type.GetType on a type name retrieved from the resource stream:

  • LoadString calls FindType, and checks that the found type was typeof(string) before reading a string from the resource stream. This could possibly be factored so that the GetString path does not use Type.GetType, but just checks the string in the stream. Reading strings is probably the most common use case.
  • LoadObjectV1 similarly calls FindType, 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 call DeserializeObject, which in turn calls FindType.

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 whether BinaryFormatter 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 in System.Runtime.Serialization.Formatters there's no direct compile time dependency. Instead the BinaryFormatter is created via reflection in ResourceReader.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 with PreserveDependency attributes.

(Kudos to @sbomer for most of the analysis on this problem)

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions