Skip to content

Introduce opt-in BinaryFormatter killbit #38963

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

Merged
merged 20 commits into from
Jul 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion docs/workflow/trimming/feature-switches.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ configurations but their defaults might vary as any SDK can set the defaults dif
| MSBuild Property Name | AppContext Setting | Description |
|-|-|-|
| DebuggerSupport | System.Diagnostics.Debugger.IsSupported | Any dependency that enables better debugging experience to be trimmed when set to false |
| EnableUnsafeUTF7Encoding | System.Text.Encoding.EnableUnsafeUTF7Encoding | Insecure UTF-7 encoding is trimmed when set to false |
| EnableUnsafeUTF7Encoding | System.Text.Encoding.EnableUnsafeUTF7Encoding | Insecure UTF-7 encoding is trimmed when set to false |
| EnableUnsafeBinaryFormatterSerialization | System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization | BinaryFormatter serialization support is trimmed when set to false |
| EventSourceSupport | System.Diagnostics.Tracing.EventSource.IsSupported | Any EventSource related code or logic is trimmed when set to false |
| InvariantGlobalization | System.Globalization.Invariant | All globalization specific code and data is trimmed when set to true |
| UseSystemResourceKeys | System.Resources.UseSystemResourceKeys | Any localizable resources for system assemblies is trimmed when set to true |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ private static bool GetSwitchDefaultValue(string switchName)
return true;
}

if (switchName == "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization")
{
return true;
}

return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace System.Collections.Tests
{
public abstract partial class IEnumerable_Generic_Tests<T> : TestBase<T>
{
[Theory]
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
[MemberData(nameof(ValidCollectionSizes))]
public void IGenericSharedAPI_SerializeDeserialize(int count)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace System.Collections.Tests
{
public abstract partial class IEnumerable_NonGeneric_Tests : TestBase
{
[Theory]
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
[MemberData(nameof(ValidCollectionSizes))]
public void IGenericSharedAPI_SerializeDeserialize(int count)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace System.Collections.Generic.Tests
{
public abstract partial class ComparersGenericTests<T>
{
[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
public void EqualityComparer_SerializationRoundtrip()
{
var bf = new BinaryFormatter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ private static IDictionary<T, T> CreateDictionary<T>(int size, Func<int, T> keyV
return dict;
}

[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
public void ComparerSerialization()
{
// Strings switch between randomized and non-randomized comparers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ public void Remove_NonDefaultComparer_ComparerUsed(int capacity)

#region Serialization

[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
public void ComparerSerialization()
{
// Strings switch between randomized and non-randomized comparers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public static void Ctor_String_ValidationAttribute_Object(string errorMessage)
Assert.Same(value, ex.Value);
}

[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
public void Ctor_SerializationInfo_StreamingContext()
{
using (var stream = new MemoryStream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void Ctor_String_Exception(string message)
Assert.Null(exception.ParamName);
}

[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
public void Ctor_SerializationInfo_StreamingContext()
{
using (var stream = new MemoryStream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public void Ctor_NullEnumClass_ThrowsArgumentNulException()
AssertExtensions.Throws<ArgumentNullException, NullReferenceException>("enumClass", () => new InvalidEnumArgumentException("argumentName", 1, null));
}

[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
public void Ctor_SerializationInfo_StreamingContext()
{
using (var stream = new MemoryStream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void Cancelled_Get_ReturnsExpected()
Assert.Null(exception.InnerException);
}

[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
public void Ctor_SerializationInfo_StreamingContext()
{
using (var stream = new MemoryStream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public void Ctor_Type_Object_String_Exception(Type type, object instance, string
Assert.Equal(message, exception.Message);
}

[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
public void Ctor_SerializationInfo_StreamingContext()
{
using (var stream = new MemoryStream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public void Ctor_String_String_String(string message, string helpUrl, string hel
Assert.Equal(message, exception.Message);
}

[Fact]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
public void Ctor_SerializationInfo_StreamingContext()
{
using (var stream = new MemoryStream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
<type fullname="System.LocalAppContextSwitches">
<method signature="System.Boolean get_EnableUnsafeUTF7Encoding()" body="stub" value="false" feature="System.Text.Encoding.EnableUnsafeUTF7Encoding" featurevalue="false" />
</type>
<type fullname="System.Resources.ResourceReader">
<method signature="System.Boolean InitializeBinaryFormatter()" body="stub" value="false" feature="System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization" featurevalue="false" />
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

@marek-safar You're referring to the "kitchen sink" test project in the sdk repo, right?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, what Jan said

Copy link
Member

Choose a reason for hiding this comment

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

This shouldn't be on ResourceReader, the substitution should be on System.LocalAppContextSwitches::get_BinaryFormatterEnabled(). That way all other code that is conditioned on LocalAppContextSwitches.BinaryFormatterEnabled gets trimmed as well.

Copy link
Member

Choose a reason for hiding this comment

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

We should not need LocalAppContextSwitches::get_BinaryFormatterEnabled in CoreLib at all. That's why I have suggested the substitution to be on InitializeBinaryFormatter

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, Jan, I think I see what you mean. One sec while I send a new iteration.

</type>
</assembly>
</linker>
Original file line number Diff line number Diff line change
Expand Up @@ -3757,4 +3757,7 @@
<data name="Arg_MustBeHalf" xml:space="preserve">
<value>Object must be of type Half.</value>
</data>
<data name="BinaryFormatter_SerializationDisallowed" xml:space="preserve">
<value>BinaryFormatter serialization and deserialization are disabled within this application. See https://aka.ms/binaryformatter for more information.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Serialization\SafeSerializationEventArgs.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Serialization\SerializationException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Serialization\SerializationInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Serialization\SerializationInfo.SerializationGuard.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Serialization\SerializationInfoEnumerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Serialization\StreamingContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Runtime\Versioning\ComponentGuaranteesAttribute.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;

Expand Down Expand Up @@ -50,7 +51,14 @@ private object DeserializeObject(int typeIndex)

if (_binaryFormatter == null)
{
InitializeBinaryFormatter();
if (!InitializeBinaryFormatter())
Copy link
Member

Choose a reason for hiding this comment

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

I'm not seeing this code pattern handled in an optimal way by the linker. Here's the resulting trimmed code:

        private object DeserializeObject(int typeIndex)
        {
            if (!this._permitDeserialization)
            {
                throw new NotSupportedException(SR.NotSupported_ResourceObjectSerialization);
            }
            if (this._binaryFormatter == null)
            {
                this.InitializeBinaryFormatter();
                throw new NotSupportedException(SR.BinaryFormatter_SerializationDisallowed);
            }
            Type type = this.FindType(typeIndex);
            object sDeserializeMethod = ResourceReader.s_deserializeMethod(this._binaryFormatter, this._store.BaseStream);
            if (sDeserializeMethod.GetType() != type)
            {
                throw new BadImageFormatException(SR.Format(SR.BadImageFormat_ResType_SerBlobMismatch, type.FullName, sDeserializeMethod.GetType().FullName));
            }
            return sDeserializeMethod;
        }

Since the switch is cut off inside of the if (_binaryFormatter == null), the linker won't trim the other code in this method. Being able to trim the rest of the code in this method is nice because other things can be trimmed as well, like s_deserializeMethod. It might not be a huge deal here since the dead code is minimal. But in general we should try to make the linker check optimal so the rest of the method can be trimmed.

Copy link
Member Author

Choose a reason for hiding this comment

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

@eerhardt Would something like this work? I can send it as a separate PR.

object binaryFormatter = GetBinaryFormatter(); // hardcoded to return null on wasm
if (binaryFormatter is null)
{
    throw new PNSE();
}

Type type = this.FindType(typeIndex);
// remainder of method

private object BinaryFormatter()
{
    if (_binaryFormatter is null)
    {
        // initialize
    }

    Debug.Assert(_binaryFormatter != null);
    return _binaryFormatter;
}

Copy link
Member

Choose a reason for hiding this comment

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

I do not think the linker can stub methods that return object: https://github.com/mono/linker/blob/811c69dfb6e325a344b713890408b137b39a888b/src/linker/Linker.Steps/BodySubstituterStep.cs#L172

I think the easiest way to fix this is to move if (this._binaryFormatter == null) inside InitializeBinaryFormatter. Also, I think it would be fine to defer this to #32862 . The InitializeBinaryFormatter method will need to be rewritten to make it work well with reflection linking and analysis.

Copy link
Member

Choose a reason for hiding this comment

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

Note that #32862 is not planned for 5.0.

I am also fine with deferring this right now. I don't see a lot of code being preserved here since the FindType method is also called elsewhere. So the size savings won't be impactful.

{
// The linker trimmed away the BinaryFormatter implementation and we can't call into it.
// We'll throw an exception with the same text that BinaryFormatter would have thrown
// had we been able to call into it. Keep this resource string in sync with the same
// resource from the Formatters assembly.
throw new NotSupportedException(SR.BinaryFormatter_SerializationDisallowed);
}
}

Type type = FindType(typeIndex);
Expand All @@ -65,8 +73,11 @@ private object DeserializeObject(int typeIndex)
}

// Issue https://github.com/dotnet/runtime/issues/39290 tracks finding an alternative to BinaryFormatter
private void InitializeBinaryFormatter()
private bool InitializeBinaryFormatter()
{
// If BinaryFormatter support is disabled for the app, the linker will replace this entire
// method body with "return false;", skipping all reflection code below.

LazyInitializer.EnsureInitialized(ref s_binaryFormatterType, () =>
Type.GetType("System.Runtime.Serialization.Formatters.Binary.BinaryFormatter, System.Runtime.Serialization.Formatters, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
throwOnError: true)!);
Expand All @@ -83,6 +94,8 @@ private void InitializeBinaryFormatter()
});

_binaryFormatter = Activator.CreateInstance(s_binaryFormatterType!)!;

return true; // initialization successful
}

// generic method that we specialize at runtime once we've loaded the BinaryFormatter type
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security;
using System.Threading;

namespace System.Runtime.Serialization
{
/// <summary>The structure for holding all of the data needed for object serialization and deserialization.</summary>
public sealed partial class SerializationInfo
{
internal static AsyncLocal<bool> AsyncDeserializationInProgress { get; } = new AsyncLocal<bool>();

#if !CORECLR
// On AoT, assume private members are reflection blocked, so there's no further protection required
// for the thread's DeserializationTracker
[ThreadStatic]
private static DeserializationTracker? t_deserializationTracker;

private static DeserializationTracker GetThreadDeserializationTracker() =>
t_deserializationTracker ??= new DeserializationTracker();
#endif // !CORECLR

// Returns true if deserialization is currently in progress
public static bool DeserializationInProgress
{
#if CORECLR
[DynamicSecurityMethod] // Methods containing StackCrawlMark local var must be marked DynamicSecurityMethod
#endif
get
{
if (AsyncDeserializationInProgress.Value)
{
return true;
}

#if CORECLR
StackCrawlMark stackMark = StackCrawlMark.LookForMe;
DeserializationTracker tracker = Thread.GetThreadDeserializationTracker(ref stackMark);
#else
DeserializationTracker tracker = GetThreadDeserializationTracker();
#endif
bool result = tracker.DeserializationInProgress;
return result;
}
}

// Throws a SerializationException if dangerous deserialization is currently
// in progress
public static void ThrowIfDeserializationInProgress()
{
if (DeserializationInProgress)
{
throw new SerializationException(SR.Serialization_DangerousDeserialization);
}
}

// Throws a DeserializationBlockedException if dangerous deserialization is currently
// in progress and the AppContext switch Switch.System.Runtime.Serialization.SerializationGuard.{switchSuffix}
// is not true. The value of the switch is cached in cachedValue to avoid repeated lookups:
// 0: No value cached
// 1: The switch is true
// -1: The switch is false
public static void ThrowIfDeserializationInProgress(string switchSuffix, ref int cachedValue)
{
const string SwitchPrefix = "Switch.System.Runtime.Serialization.SerializationGuard.";
if (switchSuffix == null)
{
throw new ArgumentNullException(nameof(switchSuffix));
}
if (string.IsNullOrWhiteSpace(switchSuffix))
{
throw new ArgumentException(SR.Argument_EmptyName, nameof(switchSuffix));
}

if (cachedValue == 0)
{
if (AppContext.TryGetSwitch(SwitchPrefix + switchSuffix, out bool isEnabled) && isEnabled)
{
cachedValue = 1;
}
else
{
cachedValue = -1;
}
}

if (cachedValue == 1)
{
return;
}
else if (cachedValue == -1)
{
if (DeserializationInProgress)
{
throw new SerializationException(SR.Format(SR.Serialization_DangerousDeserialization_Switch, SwitchPrefix + switchSuffix));
}
}
else
{
throw new ArgumentOutOfRangeException(nameof(cachedValue));
}
}

// Declares that the current thread and async context have begun deserialization.
// In this state, if the SerializationGuard or other related AppContext switches are set,
// actions likely to be dangerous during deserialization, such as starting a process will be blocked.
// Returns a DeserializationToken that must be disposed to remove the deserialization state.
#if CORECLR
[DynamicSecurityMethod] // Methods containing StackCrawlMark local var must be marked DynamicSecurityMethod
#endif
public static DeserializationToken StartDeserialization()
{
if (LocalAppContextSwitches.SerializationGuard)
{
#if CORECLR
StackCrawlMark stackMark = StackCrawlMark.LookForMe;
DeserializationTracker tracker = Thread.GetThreadDeserializationTracker(ref stackMark);
#else
DeserializationTracker tracker = GetThreadDeserializationTracker();
#endif
if (!tracker.DeserializationInProgress)
{
lock (tracker)
{
if (!tracker.DeserializationInProgress)
{
AsyncDeserializationInProgress.Value = true;
tracker.DeserializationInProgress = true;
return new DeserializationToken(tracker);
}
}
}
}

return new DeserializationToken(null);
}
}
}
Loading