-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Introduce opt-in BinaryFormatter killbit #38963
Conversation
- Also disallow BinaryFormatter from within wasm projects
src/mono/netcore/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.wasm.xml
Outdated
Show resolved
Hide resolved
@@ -141,6 +142,9 @@ internal static unsafe void Setup(char** pNames, char** pValues, int count) | |||
{ | |||
s_dataStore.Add(new string(pNames[i]), new string(pValues[i])); | |||
} | |||
|
|||
// burn these values in to the SecureAppContext's cctor | |||
RuntimeHelpers.RunClassConstructor(typeof(SecureAppContext).TypeHandle); |
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.
@jkotas @MichalStrehovsky Can you comment on whether this is the right way to run the cctor once AppContext
initialization is complete? In a nutshell: I'm trying to capture what the values were immediately upon app start, independent of any future modifications to AppContext
itself. See the code comments on SecureAppContext
for more info.
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 do not think we should be doing measures like this.
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.
Also, a better way to trigger a static constructor is to just call a static method on the type.
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 do not think we should be doing measures like this.
If somebody managed to hack this via reflection, reflection calls will run the static constructor first and then fail the attempt to set it.
a better way to trigger a static constructor is to just call a static method on the type.
The static constructor needs to be explicit for this to work.
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 don't know much about the BinaryFormatter
exploits, but if somebody already owns the process to the extent that they can flip the switches within the normal "unsecure" AppContext (they can invoke arbitrary static methods and provide parameters), I would think their level of interest in BinaryFormatter
would be pretty low because there's plenty other surface to attack at that point (WebClient.DownloadFile
sounds much more convenient than AppContext.SetSwitch
followed by complex BinaryFormatter
goo).
/// process. Arbitrary memory writes can also alter these fields. Both of these scenarios are | ||
/// outside the scope of our threat model. | ||
/// </remarks> | ||
internal static class SecureAppContext |
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 would make this local to System.Runtime.Serialization.Formatters. I do not think it needs to live in CoreLib.
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 disagree, this has the basis of becoming a wider used feature for disabling things, and isn't serialization specific.
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.
If you want this to be a wider used feature for disabling things, this needs to be designed a proper public API/surface. The current shape is not suitable as public API.
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.
At least some part of this needs to live in System.Private.CoreLib, as it's required to be able to trim away the reflection logic in ResourceReader.Core.cs.
src/libraries/System.Private.CoreLib/src/System/SecureAppContext.cs
Outdated
Show resolved
Hide resolved
The tests failing on Mono should be disabled against #37899 |
The tests getting enabled in #38948 will need to be disabled again. cc @akoeplinger |
1. Added: (http://web.mit.edu/jemorris/humor/500-miles) 2. Added: JustWatch - The Streaming Guide (https://www.justwatch.com/) 3. Added: Introduce opt-in BinaryFormatter killbit by GrabYourPitchforks · Pull Request #38963 · dotnet/runtime (dotnet/runtime#38963)
src/libraries/System.Runtime.Serialization.Formatters/tests/DisableBitTests.cs
Outdated
Show resolved
Hide resolved
I think we should just skip the whole test assembly on Mono for Browser EDIT: I thought there was only BinaryFormatter tests but that was a wrong assumption, we should just skip tests that use BinaryFormatter. |
Depends on what you call "standard reflection". Is this standard reflection? class FortKnox
{
private static readonly bool s_unbreakableLock;
static FortKnox()
{
s_unbreakableLock = InsecureJunk.s_insecureFlag;
}
public static void Enter()
{
Console.WriteLine(s_unbreakableLock ? "Go away" : "Open sesame");
}
}
class InsecureJunk
{
internal static bool s_insecureFlag;
static void Main()
{
s_insecureFlag = true;
FortKnox.Enter();
s_insecureFlag = false;
var del = (Action)Delegate.CreateDelegate(typeof(Action), typeof(FortKnox), ".cctor");
del();
FortKnox.Enter();
}
} I must say I don't know what the I would like us to eventually have an API on
|
I do not think this should be part of the pull request. If we want a feature like this, it should be designed and approved as a proper public API. I am actually not sure whether it provides enough value. Today, if you set AppContext value mid-flight after somebody cached it, the new value will be simply ignored. With this feature, you would get an exception telling you that the value is ignored. |
src/libraries/System.Runtime.Serialization.Formatters/src/Resources/Strings.resx
Outdated
Show resolved
Hide resolved
src/libraries/System.Runtime/tests/System/AppContext/SecureAppContext.cs
Outdated
Show resolved
Hide resolved
@@ -2,8 +2,10 @@ | |||
<PropertyGroup> | |||
<AssemblyName>System.Runtime.Serialization.Formatters</AssemblyName> | |||
<RootNamespace>System.Runtime.Serialization.Formatters</RootNamespace> | |||
<TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks> | |||
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppCurrent)-Browser</TargetFrameworks> |
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'm not sure this is worth it, IL linker will do the same job with code you already wrote
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.
The split is to throw different exception for the unconditionally disabled variant: #38963 (comment)
I am not sure whether the different exception is worth the complexity for the unconditionally disabled variant. But if we do want the unconditionally disabled variant to be different, we can go all the way and make the whole type disabled: #38963 (comment)
@@ -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 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
{ | ||
if (!LocalAppContextSwitches.BinaryFormatterEnabled) |
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.
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 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.
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.
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 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.
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 think there are two options:
- Throw the same exception as what binary formatter throws here
- 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.
LGTM otherwise. Thank you! |
Yes. For example, when the runtime/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.Shared.xml Lines 3 to 5 in 4cc3750
And assemblies outside of CoreLib that use EventSource get the const propagated to them and the branches are trimmed as expected. |
@@ -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 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.
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.
We should not need LocalAppContextSwitches::get_BinaryFormatterEnabled
in CoreLib at all. That's why I have suggested the substitution to be on InitializeBinaryFormatter
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.
Ah, Jan, I think I see what you mean. One sec while I send a new iteration.
Edit: Nevermind. Turns out this is the easiest way to make the wasm implementation always throw. Otherwise we have to contort the build to introduce a wasm-specific linker file. |
src/mono/netcore/System.Private.CoreLib/src/ILLink/ILLink.Substitutions.wasm.xml
Outdated
Show resolved
Hide resolved
src/libraries/System.Runtime.Serialization.Formatters/src/ILLink/ILLink.Substitutions.xml
Show resolved
Hide resolved
@jkotas, I think bcda8cb addresses your feedback. If the linker substitutes "return false" for the Initialize method body, we can't rely on BinaryFormatter.Deserialize to throw an exception (since we can't even call it), so we'll throw an exception with the same text. If the linker does not substitute "return false", we'll call into BF. And if it throws an exception, so be it. |
Yes, this is exactly what I meant. Thanks! |
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 would be ok be having the trimming test in a follow up PR, together with the work to make reflection pattern in ResourceReader understood by the linker. It is tracked by #32862
@@ -50,7 +51,14 @@ private object DeserializeObject(int typeIndex) | |||
|
|||
if (_binaryFormatter == null) | |||
{ | |||
InitializeBinaryFormatter(); | |||
if (!InitializeBinaryFormatter()) |
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'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.
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.
@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 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.
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.
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.
CI failures are known issue: #39404. |
This is the first stage of the long-term plan to begin winding down
BinaryFormatter
within the framework. Contributes to #29976. That issue is a bit outdated, and we're cleaning up the new long-term plan before publishing it. But wanted to at least get this in since there's agreement on the team that it's valuable.This PR introduces the following changes:
BinaryFormatter
via the .csproj or your application's configBinaryFormatter
infrastructureBinaryFormatter
completely in wasm projects, as it wasn't intended to work in those scenariosAppContext
switches as "sensitive" such that they cannot be overwritten using standard reflectionThere are no public API changes as part of this PR. I've added the "needs doc" label anyway since this introduces new behaviors that should be noted in the API docs.
Opting in to the killbit via config
For applications which don't anticipate using any
BinaryFormatter
code paths, theBinaryFormatter
feature can be disabled wholesale via app config or a project-wide setting. This can only be done at the application level, not at the library level. This can be used to reduce the overall output size of the compiled application, or it can be done as part of an overall threat surface reduction. The threat surface reduction stems from the elimination of normally-untaken code paths that call into theBinaryFormatter
infrastructure and that an adversary might discover how to activate, against the app developer's intentions.At this time we don't anticipate app developers opting in to the killbit in WinForms apps or other project types where the runtime itself makes use of
BinaryFormatter
. Those scenarios will likely fail in unexpected ways.Via
runtimeConfig.template.json
:Or via your application's .csproj:
(Note: the csproj setting requires changes in the runtime/sdk repo, which aren't yet submitted.)
Impact to WASM projects
The killbit is always active in WASM (Blazor client) projects, with no ability to opt out. These project types should switch to a safe, linker-friendly serialization stack like System.Text.Json.