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

Prohibit BF unless the app opts in #48527

Merged
merged 19 commits into from
Mar 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/workflow/trimming/feature-switches.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ configurations but their defaults might vary as any SDK can set the defaults dif
| StartupHookSupport | System.StartupHookProvider.IsSupported | Startup hooks are disabled when set to false. Startup hook related functionality can be trimmed. |
| TBD | System.Threading.ThreadPool.EnableDispatchAutoreleasePool | When set to true, creates an NSAutoreleasePool around each thread pool work item on applicable platforms. |
| CustomResourceTypesSupport | System.Resources.ResourceManager.AllowCustomResourceTypes | Use of custom resource types is disabled when set to false. ResourceManager code paths that use reflection for custom types can be trimmed. |
| EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization | System.ComponentModel.TypeConverter.EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization | BinaryFormatter serialization support is trimmed when set to false. |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to open a PR in dotnet/sdk to add this MSBuild property.


Any feature-switch which defines property can be set in csproj file or
on the command line as any other MSBuild property. Those without predefined property name
Expand Down
1 change: 1 addition & 0 deletions eng/testing/linker/project.csproj.template
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

<ItemGroup>
{AdditionalProjectReferences}
{TrimmerRootAssemblies}
</ItemGroup>

<!-- Logic to override the default IlLink tasks that come from the SDK and use the one
Expand Down
6 changes: 6 additions & 0 deletions eng/testing/linker/trimmingTests.targets
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
<_additionalProjectSourceFiles Include="%(TestConsoleApps.AdditionalSourceFiles)" />
</ItemGroup>

<PropertyGroup>
<_TrimmerRootAssemblies>%(TestConsoleApps.TrimmerRootAssemblies)</_TrimmerRootAssemblies>
<_TrimmerRootAssemblies Condition="'$(_TrimmerRootAssemblies)' != ''">&lt;TrimmerRootAssembly Include=&quot;$(_TrimmerRootAssemblies)&quot; /&gt;</_TrimmerRootAssemblies>
</PropertyGroup>

<MakeDir Directories="$(_projectDir)" />
<WriteLinesToFile File="$(_projectFile)"
Lines="$([System.IO.File]::ReadAllText('$(ProjectTemplate)')
Expand All @@ -77,6 +82,7 @@
.Replace('{MicrosoftNETILLinkTasksVersion}', '$(MicrosoftNETILLinkTasksVersion)')
.Replace('{ExtraTrimmerArgs}', '%(TestConsoleApps.ExtraTrimmerArgs)')
.Replace('{AdditionalProjectReferences}', '$(_additionalProjectReferencesString)')
.Replace('{TrimmerRootAssemblies}', '$(_TrimmerRootAssemblies)')
.Replace('{RepositoryEngineeringDir}', '$(RepositoryEngineeringDir)')
.Replace('{MonoAOTCompilerDir}', '$(MonoAOTCompilerDir)')
.Replace('{MonoProjectRoot}', '$(MonoProjectRoot)')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<linker>
<assembly fullname="System.ComponentModel.TypeConverter">
<type fullname="System.ComponentModel.Design.DesigntimeLicenseContextSerializer" feature="System.ComponentModel.TypeConverter.EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization" featurevalue="false">
<method signature="System.Boolean get_EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization()" body="stub" value="false" />
</type>
</assembly>
</linker>
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<linker>
<assembly fullname="System.ComponentModel.TypeConverter, PublicKeyToken=b03f5f7f11d50a3a">
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2026</argument>
<property name="Scope">member</property>
<property name="Target">M:System.ComponentModel.Design.DesigntimeLicenseContextSerializer.Deserialize(System.IO.Stream,System.String,System.ComponentModel.Design.RuntimeLicenseContext)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2026</argument>
<property name="Scope">member</property>
<property name="Target">M:System.ComponentModel.Design.DesigntimeLicenseContextSerializer.Serialize(System.IO.Stream,System.String,System.ComponentModel.Design.DesigntimeLicenseContext)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2072</argument>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,7 @@
<data name="MemberRelationshipService_RelationshipNotSupported" xml:space="preserve">
<value>Relationships between {0}.{1} and {2}.{3} are not supported.</value>
</data>
<data name="BinaryFormatterMessage" xml:space="preserve">
<value>BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<IsPartialFacadeAssembly>true</IsPartialFacadeAssembly>
<TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ILLinkSubstitutionsXmls Include="$(ILLinkDirectory)ILLink.Substitutions.xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="MS\Internal\Xml\Linq\ComponentModel\XComponentModel.cs" />
<Compile Include="System\ComponentModel\ArrayConverter.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Runtime.Serialization;
using System.Collections;
using System.IO;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace System.ComponentModel.Design
Expand All @@ -14,6 +15,10 @@ namespace System.ComponentModel.Design
/// </summary>
public class DesigntimeLicenseContextSerializer
{
internal const byte BinaryWriterMagic = 255;

private static bool EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization { get; } = AppContext.TryGetSwitch("System.ComponentModel.TypeConverter.EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization", out bool isEnabled) ? isEnabled : false;

// Not creatable.
private DesigntimeLicenseContextSerializer()
{
Expand All @@ -24,26 +29,156 @@ private DesigntimeLicenseContextSerializer()
/// using the specified key and output stream.
/// </summary>
public static void Serialize(Stream o, string cryptoKey, DesigntimeLicenseContext context)
{
if (EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization)
{
SerializeWithBinaryFormatter(o, cryptoKey, context);
}
else
{
using (BinaryWriter writer = new BinaryWriter(o, encoding: Text.Encoding.UTF8, leaveOpen: true))
{
writer.Write(BinaryWriterMagic); // flag to identify BinaryWriter
writer.Write(cryptoKey);
writer.Write(context._savedLicenseKeys.Count);
foreach (DictionaryEntry keyAndValue in context._savedLicenseKeys)
{
writer.Write(keyAndValue.Key.ToString());
writer.Write(keyAndValue.Value.ToString());
}
}
}
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "Only simple types (string and HashTable) are serialized with BinaryFormatter. These types can be serialized in a trimmed application.")]
private static void SerializeWithBinaryFormatter(Stream o, string cryptoKey, DesigntimeLicenseContext context)
pgovind marked this conversation as resolved.
Show resolved Hide resolved
{
IFormatter formatter = new BinaryFormatter();
#pragma warning disable SYSLIB0011 // Issue https://github.com/dotnet/runtime/issues/39293 tracks finding an alternative to BinaryFormatter
#pragma warning disable SYSLIB0011
formatter.Serialize(o, new object[] { cryptoKey, context._savedLicenseKeys });
#pragma warning restore SYSLIB0011
}

internal static void Deserialize(Stream o, string cryptoKey, RuntimeLicenseContext context)
private class StreamWrapper : Stream
{
#pragma warning disable SYSLIB0011 // Issue https://github.com/dotnet/runtime/issues/39293 tracks finding an alternative to BinaryFormatter
IFormatter formatter = new BinaryFormatter();
private Stream _stream;
private bool _readFirstByte;
internal byte _firstByte;
public StreamWrapper(Stream stream)
{
_stream = stream;
_readFirstByte = false;
_firstByte = 0;
}

public override bool CanRead => _stream.CanRead;

public override bool CanSeek => _stream.CanSeek;

public override bool CanWrite => _stream.CanWrite;

public override long Length => _stream.Length;

public override long Position { get => _stream.Position; set => _stream.Position = value; }

public override void Flush() => _stream.Flush();

public override int Read(byte[] buffer, int offset, int count)
{
Debug.Assert(_stream.Position != 0, "Expected the first byte to be read first");
if (_stream.Position == 1)
{
Debug.Assert(_readFirstByte == true);
// Add the first byte read by ReadByte into buffer here
buffer[offset] = _firstByte;
return _stream.Read(buffer, offset + 1, count - 1) + 1;
}
return _stream.Read(buffer, offset, count);
}

public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin);

public override void SetLength(long value) => _stream.SetLength(value);

object obj = formatter.Deserialize(o);
public override void Write(byte[] buffer, int offset, int count) => _stream.Write(buffer, offset, count);

public override int ReadByte()
pgovind marked this conversation as resolved.
Show resolved Hide resolved
{
byte read = (byte)_stream.ReadByte();
_firstByte = read;
_readFirstByte = true;
return read;
}
}

/// <summary>
/// During deserialization, the stream passed in may be binary formatted or may have used binary writer. This is a quick test to discern between them.
/// </summary>
private static bool StreamIsBinaryFormatted(StreamWrapper stream)
{
// For binary formatter, the first byte is the SerializationHeaderRecord and has a value 0
int firstByte = stream.ReadByte();
if (firstByte != 0)
{
return false;
}

return true;
}

[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicConstructors, typeof(Hashtable))]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that the Hashtable constructors are going to be kept even when EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization is disabled. This is not linker friendly.

We generally do not care about linker compatibility with binary serialization enabled. Could you please delete this DynamicDependency if you agree?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not care about linker compatibility with binary serialization enabled

What do you mean by linker compatibility here?

If I remove the DynamicDependency here, an app with binary serialization enabled will fail at runtime because Reflection will try to construct a HashTable and fail to find the right constructor?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that the Hashtable constructors are going to be kept even when EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization is disabled.

The Hashtable constructors will only be preserved if EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization is explicitly enabled. @pgovind is going to add a follow up PR in dotnet/sdk which will set EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization=false by default when PublishTrimmed=true.

This follows the same pattern with other feature switches that are set by default for trimmed apps:

  • StartupHookSupport
  • CustomResourceTypesSupport

See https://github.com/dotnet/sdk/blob/f922d2ef62dcaf6b99e64ed2ff48f772ad081d86/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets#L23-L41

Copy link
Member

@eerhardt eerhardt Mar 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I take that back... the check for the feature switch is inside this method. This needs to be refactored so the method attributed with [DynamicDependency] is only called within the feature switch check.

Nice catch @jkotas!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remove the DynamicDependency here, an app with binary serialization enabled will fail at runtime because Reflection will try to construct a HashTable and fail to find the right constructor?

It is fine for an trimmed app with binary serialization enabled to fail at the runtime. If you enable BinarySerialization you will get a lot linker warnings about non-analyzable reflection uses. We do not plan to fix those warnings. BinarySerialization is deprecated feature and it is fine that it does not work with trimming.
We have been deleting old code that was trying to make the binary serialization compatible with trimming, example dotnet/linker#1925.

It is why I am suggesting to just delete this [DynamicDependency]. Deleting it would be consistent with approach we use everywhere else wrt binary serialization.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, if we are not cleaning up all build-time linker warnings for given scenario, we should make no affort to make the scenario work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am saying that we should be doing same thing here as what we are doing for CustomResourceTypesSupport.

I agree. And for CustomResourceTypesSupport, we are going to need to make it work in the context of WinForms apps. Or else WinForms apps that use resources (pretty much any real app) plainly won't work once trimmed. And WinForms, the owners of the scenario, will have to figure out the right things to preserve to make it work.

Here - we are the owners of the scenario. The runtime is calling this code. I don't think it is unreasonable for us to figure out what are the right things to preserve here - especially since it is so simple.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we are not cleaning up all build-time linker warnings for given scenario

We did clean up the linker warnings for this scenario, because this was so simple - it is just (de)serialization string and Hashtable. And we added a trimming test to ensure it works.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And for CustomResourceTypesSupport, we are going to need to make it work in the context of WinForms apps.

And the scheme to make it work should not be based on binary serializer to deliver on https://github.com/dotnet/designs/blob/main/accepted/2020/better-obsoletion/binaryformatter-obsoletion.md#net-6-nov-2021

We did clean up the linker warnings for this scenario, because this was so simple

We should not be spending any time on improving binary serializer paths, even if it is simple. It is not a good use of time and it creates conflicting message about aggresively deprecating binary serializer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I just felt that if we save a customer a few hours of trying to figure out why their app doesn't work, it would be worth it.

We can delete this attribute, plus the tests. and add the ILLink warnings to an ILLink.Suppressions.LibraryBuild.xml file.

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "HashTable's Serialization ctor will be preserved by the DynamicDependency.")]
private static void DeserializeUsingBinaryFormatter(StreamWrapper wrappedStream, string cryptoKey, RuntimeLicenseContext context)
{
if (EnableUnsafeBinaryFormatterInDesigntimeLicenseContextSerialization)
{
#pragma warning disable SYSLIB0011
IFormatter formatter = new BinaryFormatter();

object obj = formatter.Deserialize(wrappedStream);
#pragma warning restore SYSLIB0011

if (obj is object[] value)
if (obj is object[] value)
{
if (value[0] is string && (string)value[0] == cryptoKey)
{
context._savedLicenseKeys = (Hashtable)value[1];
}
}
}
else
{
throw new NotSupportedException(SR.BinaryFormatterMessage);
}
}

internal static void Deserialize(Stream o, string cryptoKey, RuntimeLicenseContext context)
{
StreamWrapper wrappedStream = new StreamWrapper(o);
if (StreamIsBinaryFormatted(wrappedStream))
pgovind marked this conversation as resolved.
Show resolved Hide resolved
{
DeserializeUsingBinaryFormatter(wrappedStream, cryptoKey, context);
}
else
{
if (value[0] is string && (string)value[0] == cryptoKey)
using (BinaryReader reader = new BinaryReader(wrappedStream, encoding: Text.Encoding.UTF8, leaveOpen: true))
{
context._savedLicenseKeys = (Hashtable)value[1];
byte binaryWriterIdentifer = wrappedStream._firstByte;
Debug.Assert(binaryWriterIdentifer == BinaryWriterMagic, $"Expected the first byte to be {BinaryWriterMagic}");
string streamCryptoKey = reader.ReadString();
int numEntries = reader.ReadInt32();
if (streamCryptoKey == cryptoKey)
{
context._savedLicenseKeys.Clear();
for (int i = 0; i < numEntries; i++)
{
string key = reader.ReadString();
string value = reader.ReadString();
context._savedLicenseKeys.Add(key, value);
pgovind marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
}
Expand Down
Loading