unsafe byte testA()
{
var array = new byte[1];
fixed (byte* p = array)
{
*p = 22;
}
return array[0];
}
This (rather ridiculous) method should return 22. In fact, it does.
But look at a small variation:
unsafe byte testB()
{
var array = new byte[1];
fixed (byte* p = array)
{
Action action = delegate { *p = 22; };
action();
}
return array[0];
}
This should do the same thing, the difference is that the fixed pointer p is accessed inside a delegate. The action is executed synchronously inside the fixed scope, so everything should be be fine.
But it turns out that it is NOT!
When you compile it (I'm using VS2008 SP1 and .NET 3.5 SP1) and look at the compiled version of the method, it looks like this (disassembled using Reflector):
private unsafe byte testB()
{
byte[] CS$0$0001;
byte[] array = new byte[1];
byte* p;
if (((CS$0$0001 = array) == null) || (CS$0$0001.Length == 0))
{
p = null;
}
else
{
p = CS$0$0001;
}
Action action = delegate {
p[0] = 0x16;
};
action();
p = null;
return array[0];
}
I don't know what this CS$0$0001 is about, but that's secondary.
The importing thing is: there's no fixed statement!!!
Without the fixed statement the array can be moved in memory any time, and therefore the unsafe code can fail!
It looks like that the C# compiler think it can optimize away the fixed statement. For some reason it doesn't take the usage of p in the delegate into account. Of course, one could execute the delegate outside the scope of the fixed statement. The compiler doesn't have a chance to prolong the lifetime of the pinned pointer until then. But this is not the case here.
This is not a theoretical problem. We ran into this in a real-world application.
Here's a small test program that illustrates that:
static unsafe byte test()
{
var array = new byte[1];
fixed (byte* p = array)
{
Action action = delegate { *p = 22; };
Thread.Sleep(10);
action();
}
return array[0];
}
static void Main()
{
for (int i = 0; i < 10000; i++)
{
if (22 != test())
{
Console.WriteLine("Run #{0} failed!", i);
}
}
}
This code runs our example code 10,000 times. I added a Sleep() call in my test method to make the problem more likely. On my machine the error occurs usually after around 3500 runs.
Michael Stanton, who helped my track down this issue, has more in-depth info about it.
If you looking for a fix, there is an easy work-around (if you really need both unsafe code and the delegate). You can pin your memory manually without using the fixed statement. Then you have full control over its life-time.
For our example this would be:
unsafe byte testFixed()
{
var array = new byte[1];
GCHandle handle = GCHandle.Alloc(array, GCHandleType.Pinned);
byte* p = (byte*)handle.AddrOfPinnedObject();
try
{
Action action = delegate { *p = 22; };
action();
}
finally
{
handle.Free();
}
return array[0];
}