-
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
Introduce opt-in BinaryFormatter killbit #38963
Changes from 17 commits
171a0c0
d72c8b2
64c3770
a6d2482
4900453
47c8ff6
bc6c395
241c869
906b059
c70afd0
7632fdf
6b599b2
9137e43
ac0a46a
82807b1
19b252a
5e493d6
af0f08e
bcda8cb
92cd09c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not need There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
using System.Diagnostics; | ||
using System.IO; | ||
using System.Reflection; | ||
using System.Runtime.Serialization; | ||
using System.Text; | ||
using System.Threading; | ||
|
||
|
@@ -50,7 +51,10 @@ private object DeserializeObject(int typeIndex) | |
|
||
if (_binaryFormatter == null) | ||
{ | ||
InitializeBinaryFormatter(); | ||
if (!InitializeBinaryFormatter()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think the linker can stub methods that return I think the easiest way to fix this is to move There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
{ | ||
throw new NotSupportedException(SR.NotSupported_ResourceObjectSerialization); | ||
} | ||
} | ||
|
||
Type type = FindType(typeIndex); | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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." There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there are two options:
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)!); | ||
|
@@ -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 | ||
|
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); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be great to add trimming test for this and update documentation at https://github.com/dotnet/runtime/blob/master/docs/workflow/trimming/feature-switches.md#available-feature-switches
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe Marek is referring to a test like this: https://github.com/dotnet/runtime/tree/master/src/libraries/System.Runtime/tests/TrimmingTests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, what Jan said