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

Introduce opt-in BinaryFormatter killbit #38963

Merged
merged 20 commits into from
Jul 16, 2020
Merged
Show file tree
Hide file tree
Changes from 17 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
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 @@ -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 @@ -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 @@ -41,6 +41,13 @@ public static bool PreserveEventListnerObjectIdentity
get => GetCachedSwitchValue("Switch.System.Diagnostics.EventSource.PreserveEventListnerObjectIdentity", ref s_preserveEventListnerObjectIdentity);
}

private static int s_binaryFormatterEnabled;
public static bool BinaryFormatterEnabled
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetCachedSwitchValue("System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", ref s_binaryFormatterEnabled);
}

private static int s_serializationGuard;
public static bool SerializationGuard
{
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,10 @@ 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.

{
throw new NotSupportedException(SR.NotSupported_ResourceObjectSerialization);
}
}

Type type = FindType(typeIndex);
Expand All @@ -65,8 +69,13 @@ 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 (!LocalAppContextSwitches.BinaryFormatterEnabled)
Copy link
Member

Choose a reason for hiding this comment

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

Do we really need this? Can this depend on BinaryFormater throwing the exception instead?

Copy link
Member

Choose a reason for hiding this comment

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

If you would like to provide an exception with a better error message, this can catch NotSupportedException thrown by the binary formatter.

Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps letting the PNSE / NSE bubble up would be ideal after all. It clearly indicates "I know what you were trying to do, but the current app configuration disallowed it."

Copy link
Member Author

Choose a reason for hiding this comment

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

We do want the linker to trim all of this away when BinaryFormatter is disabled, though. That's also part of the discussion at #39090. Maybe the best course of action should be to have this method throw an exception with the exact same error message that BinaryFormatter would have thrown.

Copy link
Member

Choose a reason for hiding this comment

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

I think there are two options:

  1. Throw the same exception as what binary formatter throws here
  2. Catch the NotSupportedException from the BinaryFormatter and replace it with a different exception that has better message specific to ResourceReader.

Either way is fine with me.

{
return false; // initialization failed
}

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 +92,8 @@ private void InitializeBinaryFormatter()
});

_binaryFormatter = Activator.CreateInstance(s_binaryFormatterType!)!;

return true; // initialization succeeded
}

// 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