[Mono-dev] info.AddValue(String, Object, Type) causes corrupt Binary Serialization

Andy Hume andyhume32 at yahoo.co.uk
Fri Dec 15 10:52:46 EST 2006


(Sorry about the length of this...)

I have an existing library which does:

class MySerializableClass : ISerializable
{
        // Note we store the enum value as an Int32, just in case
        // it was ever to change format...
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("FooCode", this.m_fooCode, typeof(Int32));
        }

        protected DummySerializableClass(SerializationInfo info, StreamingContext context)
        {
            this.m_fooCode = (FooByteEnum)info.GetInt32("FooCode");
        }
}

(Where m_fooCode is "enum FooByteEnum : byte { ... }").

Now in hindsight I agree that the use of the AddValue method is a bit odd, perhaps with an explicit cast, or just using AddValue(String,Int32) directly, being better...

However with binary serialization on the MSFT CLR it works, but when I run the library's unit-tests on the Mono CLR it fails at deserialization time with: "System.Runtime.Serialization.SerializationException : Unexpected binary element: 0".  Further testing shows that there is also an error when the MSFT CLR too is used to read the Mono output ("System.Runtime.Serialization.SerializationException: Binary
 stream '0' does not contain a valid BinaryHeader. Possible causes are invalid stream or object version change between serialization and deserialization.").

Manual decoding of the binary output shows that the MSFT version writes {type=Int32 value=xx-xx-xx-xx}, whereas Mono writes {type=Int32 value=FooByteEnum=xx}.  Not too surprising the deserialization fails. :-,( 

My full decoding of the output created by the initial example is as follows.  This is by the use of the nice document, http://primates.ximian.com/~lluis/dist/binary_serialization_format.htm

* MSFT 2.0.50727.42.bin
Header
00 01 00 00 00 FF FF FF  FF 01 00 00 00 00 00 00 00
Assembly
0C, 02 00 00 00     ID=2
43,
4D 6F 6E 6F 42 75 67 4E 75 6D 30 32 2C 20 56 65
72 73 69 6F 6E 3D 31 2E 30 2E 30 2E 30 2C 20 43
75 6C 74 75 72 65 3D 6E 65 75 74 72 61 6C 2C 20
50 75 62 6C 69 63 4B 65 79 54 6F 6B 65 6E 3D 6E
75 6C 6C
    "MonoBugNum02, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
ExternalObject
05, 01 00 00 00     ID=1
2F,
4D 6F 6E 6F 42 75 67 4E 75 6D 32 2E 4D 6F 6E 6F
42 75 67 4E 75 6D 30 32  2B 44 75 6D 6D 79 53 65
72 69 61 6C 69 7A 61 62  6C 65 43 6C 61 73 73
    " MonoBugNum2.MonoBugNum02+DummySerializableClass"
01 00 00 00     Num. Fields
07, 46 6F 6F 43 6F 64 65     "FooCode"
00 type-tag=Primitive
08 type-spec=Int32
02 00 00 00    In Assembly ID=2
values[]=
02 00 00 00    (Int32)=2
End
0B

* Mono 2.0.50727.42.bin
00 01 00 00 00 FF FF FF  FF 01 00 00 00 00 00 00 00 
Assembly
0C, 02 00 00 00     ID=2
0C, 4D 6F 6E 6F 42 75 67 4E 75 6D 30 32     "MonoBugNum02"
ExternalObject
05, 01 00 00 00     ID=1
2F, 4D 6F 6E 6F 42 75 67 4E 75 6D 32 2E 4D 6F 6E  6F
42 75 67 4E 75 6D 30 32 2B 44 75 6D 6D 79 53  65
72 69 61 6C 69 7A 61 62 6C 65 43 6C 61 73 73 
    " MonoBugNum2.MonoBugNum02+DummySerializableClass"
01 00 00 00     NumFields=1
07, 46 6F 6F 43 6F 64 65     "FooCode"
00 type-tag=Primitive
08 type-spec=Int32
02 00 00 00    In Assembly ID=2
values[]=
ExternalObject
05, 03 00 00 00     ID=3
20, 4D 6F 6E 6F 42 75 67 4E 75 6D 32 2E 4D 6F 6E 6F
42 75 67 4E 75 6D 30 32 2B 46 6F 6F 45 6E 75 6D
    " MonoBugNum2.MonoBugNum02+FooEnum"
01 00 00 00     NumFields=1
07, 76 61 6C 75 65 5F 5F "value__"
00     type-tag=Primitive
02     type-spec=Byte
02 00 00 00 In Assembly ID=2
values[]=
02     (Byte)=2
End
0B

So we can see why the exception is "Unexpected binary element: 0...".  The deserializer is told that the type of the value is Int32 and thus goes and reads the value as the next four bytes.  In the corrupt case it thus reads "05, 03 00 00" as the value, which is actually the start of an block describing the enum object, and starts reading again at 
"00   20, 4D 6F, ...", with 0 not being a valid value.


Now I can't work out what the AddValue(String, Object, Type) overload is really intended for, and is intended to do.  My best guess is that one passes the Type value so that it can check that the value you pass is of the correct type for that field.  This is _slightly_ backed-up by the MSDN documentation: "The Type to associate with the current object. This parameter must always be the type of the object itself or of one of its base classes.", the summary text is: "Adds a value into the SerializationInfo store, where value is associated with name and is serialized as being of Type type."

So I set to writing a set of unit-tests to show that the MSFT version will prohibit the writing of a value that is not of the correct type.  However I couldn't get it to throw in any case, getting as far as the following without exception on writing:

    [Serializable]
    class Foo { public Foo() { } }
    [Serializable]
    class Bar { public Bar() { } }
...
            Foo value = new Foo();
            info.AddValue("Name", value, typeof(Bar));

So it (apparently) never throws!  I haven't yet looked at the output from that case to see what on earth it does write...


One way to stop this class of corrupt output is to do that that check.  Something like the following I presume:

    void AddValue(String name, Object value, Type type) {
       // Per MSDN:
       // type--"This parameter must always be the type of
       // the object itself or of one of its base classes.".
       if(!type.InstanceOf(value)) {
          throw new ArgumentException(...);
       }
       ... ...
    }

So is that the bug, or should I add one type-specific bug here: "SerializationInfo.AddValue("name", fooEnum, typeof(Int32)) creates corrupt output", and there should be another, say, "SerializationInfo.AddValue(String, Object, Type) can write different value type id and type of value" or "SerializationInfo.AddValue(String, Object, Type) doesn't block invalid value and type argument pairs"?


I've attached some of my test code.  Compile the C# file as a command-line app.  Then, if running on Windows, in a Mono Command Prompt, run RunBothWays.cmd.  It will run each of the following on both CLRs: simple round-trip test (fails on Mono), writes the output to disk named per the CLR, and then reads the opposite file back in.  The last shows that both CLRs fail on reading the Mono-produced output.  There are also some NUnit unit-tests in there two; the ones *aiming* to show that AddValue/3 would fail if passed an inconsistent value and type...


Andy
-------------- next part --------------
A non-text attachment was scrubbed...
Name: RunBothWays.cmd
Type: application/octet-stream
Size: 809 bytes
Desc: not available
Url : http://lists.ximian.com/pipermail/mono-devel-list/attachments/20061215/0e7140e4/attachment.obj 
-------------- next part --------------
An embedded and charset-unspecified text was scrubbed...
Name: MonoBugNum02Serialization.cs
Url: http://lists.ximian.com/pipermail/mono-devel-list/attachments/20061215/0e7140e4/attachment.pl 
-------------- next part --------------
An embedded and charset-unspecified text was scrubbed...
Name: Remove the txt extension, RunBothWays.cmd.txt
Url: http://lists.ximian.com/pipermail/mono-devel-list/attachments/20061215/0e7140e4/attachment.txt 


More information about the Mono-devel-list mailing list