Add BinaryFormatter auditing EventSource (#39874)
authorLevi Broderick <GrabYourPitchforks@users.noreply.github.com>
Wed, 29 Jul 2020 22:33:18 +0000 (15:33 -0700)
committerGitHub <noreply@github.com>
Wed, 29 Jul 2020 22:33:18 +0000 (15:33 -0700)
src/libraries/System.Runtime.Serialization.Formatters/src/System.Runtime.Serialization.Formatters.csproj
src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryFormatter.Core.cs
src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryFormatterEventSource.cs [new file with mode: 0644]
src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryObjectInfo.cs
src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterEventSourceTests.cs [new file with mode: 0644]
src/libraries/System.Runtime.Serialization.Formatters/tests/System.Runtime.Serialization.Formatters.Tests.csproj

index dba845f..124a497 100644 (file)
@@ -61,6 +61,7 @@
     <Compile Include="System\Runtime\Serialization\Formatters\Binary\BinaryFormatter.cs" />
     <Compile Include="System\Runtime\Serialization\Formatters\Binary\BinaryFormatter.Core.cs" Condition="'$(TargetsBrowser)' != 'true'" />
     <Compile Include="System\Runtime\Serialization\Formatters\Binary\BinaryFormatter.PlatformNotSupported.cs" Condition="'$(TargetsBrowser)' == 'true'" />
+    <Compile Include="System\Runtime\Serialization\Formatters\Binary\BinaryFormatterEventSource.cs" />
     <Compile Include="System\Runtime\Serialization\Formatters\Binary\BinaryFormatterWriter.cs" />
     <Compile Include="System\Runtime\Serialization\Formatters\Binary\BinaryObjectInfo.cs" />
     <Compile Include="System\Runtime\Serialization\Formatters\Binary\BinaryObjectReader.cs" />
index 95f281f..939b78a 100644 (file)
@@ -39,6 +39,7 @@ namespace System.Runtime.Serialization.Formatters.Binary
             };
             try
             {
+                BinaryFormatterEventSource.Log.DeserializationStart();
                 var parser = new BinaryParser(serializationStream, reader);
                 return reader.Deserialize(parser);
             }
@@ -50,6 +51,10 @@ namespace System.Runtime.Serialization.Formatters.Binary
             {
                 throw new SerializationException(SR.Serialization_CorruptedStream, e);
             }
+            finally
+            {
+                BinaryFormatterEventSource.Log.DeserializationStop();
+            }
         }
 
         [Obsolete(Obsoletions.BinaryFormatterMessage, DiagnosticId = Obsoletions.BinaryFormatterDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
@@ -73,10 +78,18 @@ namespace System.Runtime.Serialization.Formatters.Binary
                 _assemblyFormat = _assemblyFormat,
             };
 
-            var sow = new ObjectWriter(_surrogates, _context, formatterEnums, _binder);
-            BinaryFormatterWriter binaryWriter = new BinaryFormatterWriter(serializationStream, sow, _typeFormat);
-            sow.Serialize(graph, binaryWriter);
-            _crossAppDomainArray = sow._crossAppDomainArray;
+            try
+            {
+                BinaryFormatterEventSource.Log.SerializationStart();
+                var sow = new ObjectWriter(_surrogates, _context, formatterEnums, _binder);
+                BinaryFormatterWriter binaryWriter = new BinaryFormatterWriter(serializationStream, sow, _typeFormat);
+                sow.Serialize(graph, binaryWriter);
+                _crossAppDomainArray = sow._crossAppDomainArray;
+            }
+            finally
+            {
+                BinaryFormatterEventSource.Log.SerializationStop();
+            }
         }
     }
 }
diff --git a/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryFormatterEventSource.cs b/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryFormatterEventSource.cs
new file mode 100644 (file)
index 0000000..ca8efd4
--- /dev/null
@@ -0,0 +1,102 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.Tracing;
+
+namespace System.Runtime.Serialization.Formatters.Binary
+{
+    [EventSource(
+        Name = "System.Runtime.Serialization.Formatters.Binary.BinaryFormatterEventSource")]
+    internal sealed class BinaryFormatterEventSource : EventSource
+    {
+        private const int EventId_SerializationStart = 10;
+        private const int EventId_SerializationStop = 11;
+        private const int EventId_SerializingObject = 12;
+        private const int EventId_DeserializationStart = 20;
+        private const int EventId_DeserializationStop = 21;
+        private const int EventId_DeserializingObject = 22;
+
+        public static readonly BinaryFormatterEventSource Log = new BinaryFormatterEventSource();
+
+        private BinaryFormatterEventSource()
+        {
+        }
+
+        [Event(EventId_SerializationStart, Opcode = EventOpcode.Start, Keywords = Keywords.Serialization, Level = EventLevel.Informational, ActivityOptions = EventActivityOptions.Recursive)]
+        public void SerializationStart()
+        {
+            if (IsEnabled(EventLevel.Informational, Keywords.Serialization))
+            {
+                WriteEvent(EventId_SerializationStart);
+            }
+        }
+
+        [Event(EventId_SerializationStop, Opcode = EventOpcode.Stop, Keywords = Keywords.Serialization, Level = EventLevel.Informational)]
+        public void SerializationStop()
+        {
+            if (IsEnabled(EventLevel.Informational, Keywords.Serialization))
+            {
+                WriteEvent(EventId_SerializationStop);
+            }
+        }
+
+        [NonEvent]
+        public void SerializingObject(Type type)
+        {
+            Debug.Assert(type != null);
+
+            if (IsEnabled(EventLevel.Informational, Keywords.Serialization))
+            {
+                SerializingObject(type.AssemblyQualifiedName);
+            }
+        }
+
+        [Event(EventId_SerializingObject, Keywords = Keywords.Serialization, Level = EventLevel.Informational)]
+        private void SerializingObject(string? typeName)
+        {
+            WriteEvent(EventId_SerializingObject, typeName);
+        }
+
+        [Event(EventId_DeserializationStart, Opcode = EventOpcode.Start, Keywords = Keywords.Deserialization, Level = EventLevel.Informational, ActivityOptions = EventActivityOptions.Recursive)]
+        public void DeserializationStart()
+        {
+            if (IsEnabled(EventLevel.Informational, Keywords.Deserialization))
+            {
+                WriteEvent(EventId_DeserializationStart);
+            }
+        }
+
+        [Event(EventId_DeserializationStop, Opcode = EventOpcode.Stop, Keywords = Keywords.Deserialization, Level = EventLevel.Informational)]
+        public void DeserializationStop()
+        {
+            if (IsEnabled(EventLevel.Informational, Keywords.Deserialization))
+            {
+                WriteEvent(EventId_DeserializationStop);
+            }
+        }
+
+        [NonEvent]
+        public void DeserializingObject(Type type)
+        {
+            Debug.Assert(type != null);
+
+            if (IsEnabled(EventLevel.Informational, Keywords.Deserialization))
+            {
+                DeserializingObject(type.AssemblyQualifiedName);
+            }
+        }
+
+        [Event(EventId_DeserializingObject, Keywords = Keywords.Deserialization, Level = EventLevel.Informational)]
+        private void DeserializingObject(string? typeName)
+        {
+            WriteEvent(EventId_DeserializingObject, typeName);
+        }
+
+        public class Keywords
+        {
+            public const EventKeywords Serialization = (EventKeywords)1;
+            public const EventKeywords Deserialization = (EventKeywords)2;
+        }
+    }
+}
index c87d6a6..64f4e6d 100644 (file)
@@ -268,8 +268,11 @@ namespace System.Runtime.Serialization.Formatters.Binary
 
         internal string GetAssemblyString() => _binderAssemblyString ?? _cache._assemblyString;
 
-        private void InvokeSerializationBinder(SerializationBinder? binder) =>
+        private void InvokeSerializationBinder(SerializationBinder? binder)
+        {
+            BinaryFormatterEventSource.Log.SerializingObject(_objectType!);
             binder?.BindToName(_objectType!, out _binderAssemblyString, out _binderTypeName);
+        }
 
         internal void GetMemberInfo(out string[]? outMemberNames, out Type[]? outMemberTypes, out object?[]? outMemberData)
         {
@@ -392,6 +395,8 @@ namespace System.Runtime.Serialization.Formatters.Binary
 
         private void InitReadConstructor(Type objectType, ISurrogateSelector? surrogateSelector, StreamingContext context)
         {
+            BinaryFormatterEventSource.Log.DeserializingObject(objectType);
+
             if (objectType.IsArray)
             {
                 InitNoMembers();
diff --git a/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterEventSourceTests.cs b/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterEventSourceTests.cs
new file mode 100644 (file)
index 0000000..805b1ad
--- /dev/null
@@ -0,0 +1,216 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics.Tracing;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Threading;
+using Xunit;
+
+namespace System.Runtime.Serialization.Formatters.Tests
+{
+    [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
+    public static class BinaryFormatterEventSourceTests
+    {
+        private const string BinaryFormatterEventSourceName = "System.Runtime.Serialization.Formatters.Binary.BinaryFormatterEventSource";
+
+        [Fact]
+        public static void RecordsSerialization()
+        {
+            using LoggingEventListener listener = new LoggingEventListener();
+
+            BinaryFormatter formatter = new BinaryFormatter();
+            formatter.Serialize(Stream.Null, CreatePerson());
+            string[] capturedLog = listener.CaptureLog();
+
+            string[] expected = new string[]
+            {
+                "SerializationStart [Start, 00000001]: <no payload>",
+                "SerializingObject [Info, 00000001]: " + typeof(Person).AssemblyQualifiedName,
+                "SerializingObject [Info, 00000001]: " + typeof(Address).AssemblyQualifiedName,
+                "SerializationStop [Stop, 00000001]: <no payload>",
+            };
+
+            Assert.Equal(expected, capturedLog);
+        }
+
+        [Fact]
+        public static void RecordsDeserialization()
+        {
+            MemoryStream ms = new MemoryStream();
+            BinaryFormatter formatter = new BinaryFormatter();
+            formatter.Serialize(ms, CreatePerson());
+            ms.Position = 0;
+
+            using LoggingEventListener listener = new LoggingEventListener();
+            formatter.Deserialize(ms);
+            string[] capturedLog = listener.CaptureLog();
+
+            string[] expected = new string[]
+            {
+                "DeserializationStart [Start, 00000002]: <no payload>",
+                "DeserializingObject [Info, 00000002]: " + typeof(Person).AssemblyQualifiedName,
+                "DeserializingObject [Info, 00000002]: " + typeof(Address).AssemblyQualifiedName,
+                "DeserializationStop [Stop, 00000002]: <no payload>",
+            };
+
+            Assert.Equal(expected, capturedLog);
+        }
+
+        [Fact]
+        public static void RecordsNestedSerializationCalls()
+        {
+            // First, serialization
+
+            using LoggingEventListener listener = new LoggingEventListener();
+
+            MemoryStream ms = new MemoryStream();
+            BinaryFormatter formatter = new BinaryFormatter();
+            formatter.Serialize(ms, new ClassWithNestedDeserialization());
+            string[] capturedLog = listener.CaptureLog();
+            ms.Position = 0;
+
+            string[] expected = new string[]
+            {
+                "SerializationStart [Start, 00000001]: <no payload>",
+                "SerializingObject [Info, 00000001]: " + typeof(ClassWithNestedDeserialization).AssemblyQualifiedName,
+                "SerializationStart [Start, 00000001]: <no payload>",
+                "SerializingObject [Info, 00000001]: " + typeof(Address).AssemblyQualifiedName,
+                "SerializationStop [Stop, 00000001]: <no payload>",
+                "SerializationStop [Stop, 00000001]: <no payload>",
+            };
+
+            Assert.Equal(expected, capturedLog);
+            listener.ClearLog();
+
+            // Then, deserialization
+
+            ms.Position = 0;
+            formatter.Deserialize(ms);
+            capturedLog = listener.CaptureLog();
+
+            expected = new string[]
+            {
+                "DeserializationStart [Start, 00000002]: <no payload>",
+                "DeserializingObject [Info, 00000002]: " + typeof(ClassWithNestedDeserialization).AssemblyQualifiedName,
+                "DeserializationStart [Start, 00000002]: <no payload>",
+                "DeserializingObject [Info, 00000002]: " + typeof(Address).AssemblyQualifiedName,
+                "DeserializationStop [Stop, 00000002]: <no payload>",
+                "DeserializationStop [Stop, 00000002]: <no payload>",
+            };
+
+            Assert.Equal(expected, capturedLog);
+        }
+
+        private static Person CreatePerson()
+        {
+            return new Person()
+            {
+                Name = "Some Chap",
+                HomeAddress = new Address()
+                {
+                    Street = "123 Anywhere Ln",
+                    City = "Anywhere ST 00000 United States"
+                }
+            };
+        }
+
+        private sealed class LoggingEventListener : EventListener
+        {
+            private readonly Thread _activeThread = Thread.CurrentThread;
+            private readonly List<string> _log = new List<string>();
+
+            private void AddToLog(FormattableString message)
+            {
+                _log.Add(FormattableString.Invariant(message));
+            }
+
+            // Captures the current log
+            public string[] CaptureLog()
+            {
+                return _log.ToArray();
+            }
+
+            public void ClearLog()
+            {
+                _log.Clear();
+            }
+
+            protected override void OnEventSourceCreated(EventSource eventSource)
+            {
+                if (eventSource.Name == BinaryFormatterEventSourceName)
+                {
+                    EnableEvents(eventSource, EventLevel.Verbose);
+                }
+
+                base.OnEventSourceCreated(eventSource);
+            }
+
+            protected override void OnEventWritten(EventWrittenEventArgs eventData)
+            {
+                // The test project is parallelized. We want to filter to only events that fired
+                // on the current thread, otherwise we could throw off the test results.
+
+                if (Thread.CurrentThread != _activeThread)
+                {
+                    return;
+                }
+
+                AddToLog($"{eventData.EventName} [{eventData.Opcode}, {(int)eventData.Keywords & int.MaxValue:X8}]: {ParsePayload(eventData.Payload)}");
+                base.OnEventWritten(eventData);
+            }
+
+            private static string ParsePayload(IReadOnlyCollection<object> collection)
+            {
+                if (collection?.Count > 0)
+                {
+                    return string.Join("; ", collection.Select(o => o?.ToString() ?? "<null>"));
+                }
+                else
+                {
+                    return "<no payload>";
+                }
+            }
+        }
+
+        [Serializable]
+        private class Person
+        {
+            public string Name { get; set; }
+            public Address HomeAddress { get; set; }
+        }
+
+        [Serializable]
+        private class Address
+        {
+            public string Street { get; set; }
+            public string City { get; set; }
+        }
+
+        [Serializable]
+        public class ClassWithNestedDeserialization : ISerializable
+        {
+            public ClassWithNestedDeserialization()
+            {
+            }
+
+            protected ClassWithNestedDeserialization(SerializationInfo info, StreamingContext context)
+            {
+                byte[] serializedData = (byte[])info.GetValue("SomeField", typeof(byte[]));
+                MemoryStream ms = new MemoryStream(serializedData);
+                BinaryFormatter formatter = new BinaryFormatter();
+                formatter.Deserialize(ms); // should deserialize an 'Address' instance
+            }
+
+            public void GetObjectData(SerializationInfo info, StreamingContext context)
+            {
+                MemoryStream ms = new MemoryStream();
+                BinaryFormatter formatter = new BinaryFormatter();
+                formatter.Serialize(ms, new Address());
+                info.AddValue("SomeField", ms.ToArray());
+            }
+        }
+    }
+}
index 7e37cb0..f862142 100644 (file)
@@ -16,7 +16,6 @@
     <Compile Include="FormatterTests.cs" />
     <Compile Include="PlatformExtensions.cs" />
     <Compile Include="SerializationBinderTests.cs" />
-    <Compile Include="SerializationGuardTests.cs" Condition="'$(TargetFramework)' == '$(NetCoreAppCurrent)'" />
     <Compile Include="SerializationInfoTests.cs" />
     <Compile Include="SerializationTypes.cs" />
     <Compile Include="SurrogateSelectorTests.cs" />
     <Compile Include="$(CommonPath)System\Collections\Generic\ReferenceEqualityComparer.cs"
              Link="Common\System\Collections\Generic\ReferenceEqualityComparer.cs" />
   </ItemGroup>
+  <ItemGroup Condition="$(TargetFramework.StartsWith('$(NetCoreAppCurrent)'))">
+    <Compile Include="BinaryFormatterEventSourceTests.cs" />
+    <Compile Include="SerializationGuardTests.cs" />
+  </ItemGroup>
   <ItemGroup>
     <PackageReference Include="System.Data.SqlClient" Version="$(SystemDataSqlClientVersion)" />
   </ItemGroup>