This is the seventeenth part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#
Last time we saw how to abuse type system. Today we are going to do this to serialize non-serializable type (kind of).
Let’s go with this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
using System; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; namespace SerializeNonSerializable { [Serializable] public class Root { public int RootField { get; set; } public NonSerializableChild Child { get; set; } public void Print() { Console.WriteLine($"Root.Print {RootField}"); Magic(Child); } public void Magic(NonSerializableChild child) { Console.WriteLine($"Delegating to child {child.GetType()}"); child.Print(); } } public class NonSerializableChild { public int ChildField { get; set; } public void Print() { Console.WriteLine($"NonSerializableChild.Print {ChildField}"); } } class Program { static void Main(string[] args) { Root obj = new Root { RootField = 5, Child = new NonSerializableChild { ChildField = 10 } }; obj.Print(); IFormatter formatter = new BinaryFormatter(); Stream stream = new MemoryStream(); formatter.Serialize(stream, obj); stream.Close(); } } } |
Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Root.Print 5 Delegating to child SerializeNonSerializable.NonSerializableChild NonSerializableChild.Print 10 Unhandled Exception: System.Runtime.Serialization.SerializationException: Type 'SerializeNonSerializable.NonSerializableChild' in Assembly 'SerializeNonSerializable, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable. at System.Runtime.Serialization.FormatterServices.InternalGetSerializableMembers(RuntimeType type) at System.Runtime.Serialization.FormatterServices.<>c__DisplayClass9_0.<GetSerializableMembers>b__0(MemberHolder _) at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory) at System.Runtime.Serialization.FormatterServices.GetSerializableMembers(Type type, StreamingContext context) at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitMemberInfo() at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriterobjectWriter, SerializationBinder binder) at System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(WriteObjectInfo objectInfo, NameInfo memberNameInfo, NameInfo typeNameInfo) at System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Object graph, Header[] inHeaders, __BinaryWriter serWriter, Boolean fCheck) at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph, Header[] headers, Boolean fCheck) at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph) at SerializeNonSerializable.Program.Main(String[] args) in C:\Users\afish\Desktop\msp_windowsinternals\SerializeNonSerializable\Program.cs:line 83 |
Nothing surprising. We have a root object with non-serializable child and we get exception when we try serializing it.
Let’s now do similar thing as last time. Let’s create a mirror type with same binary structure but serializable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
using System; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; namespace SerializeNonSerializable { [Serializable] public class Root { public int RootField { get; set; } public NonSerializableChild Child { get; set; } public void Print() { Console.WriteLine($"Root.Print {RootField}"); Magic(Child); } public void Magic(NonSerializableChild child) { Console.WriteLine($"Delegating to child {child.GetType()}"); child.Print(); } } public class NonSerializableChild { public int ChildField { get; set; } public void Print() { Console.WriteLine($"NonSerializableChild.Print {ChildField}"); } } [Serializable] public class SerializableChild { public int ChildField { get; set; } public void Print() { Console.WriteLine($"SerializableChild.Print {ChildField}"); } } class Program { static void Main(string[] args) { Root obj = new Root { RootField = 5, Child = new NonSerializableChild { ChildField = 10 } }; var replacedChild = new SerializableChild { ChildField = 10 }; obj.Print(); unsafe { TypedReference typedReference = __makeref(obj); int* rootAddress = (int*)(*(int*)*(int*)&typedReference - 4); typedReference = __makeref(replacedChild); int* replacedAddress = (int*)(*(int*)*(int*)&typedReference - 4); *(rootAddress + 2) = ((int)replacedAddress) + 4; } obj.Print(); IFormatter formatter = new BinaryFormatter(); Stream stream = new MemoryStream(); formatter.Serialize(stream, obj); stream.Close(); } } } |
Output:
1 2 3 4 5 6 |
Root.Print 5 Delegating to child SerializeNonSerializable.NonSerializableChild NonSerializableChild.Print 10 Root.Print 5 Delegating to child SerializeNonSerializable.SerializableChild NonSerializableChild.Print 10 |
Okay, so we replace the instance with manual memory manipulation. We then call method on the root to delegate to child which is now of different type. However, methods are called correctly (as last time) and we can now serialize the tree.
So we can see that in theory we can rebuild the graph on the side, add attributes where needed and serialize everything. To deserialize we would need to do the opposite — read the graph and then create mirror objects of original types.