Search code examples
c#cilreflection.emit

Undefined behavior when trying to Memory.Slice using il.Emit


I am trying to write a dynamic method that takes ReadOnlyMemory<byte> and processes it. But I have experienced very weird behavior when trying to slice the memory. It seems like the arguments to slice (or any other function in the same context) are corrupted. Here is an top-level-statements example that reproduces the issue.

using System.Reflection.Emit;

DynamicMethod m = new DynamicMethod("EMITD_arraydeser_" + typeof(int[]), null, new Type[1] { typeof(ReadOnlyMemory<byte>) }, true);
var il = m.GetILGenerator();
il.DeclareLocal(typeof(ReadOnlyMemory<byte>));


il.Emit(OpCodes.Ldarg, 0);
il.Emit(OpCodes.Ldc_I4, 0);
il.Emit(OpCodes.Ldc_I4, 4);
il.EmitWriteLine("checkSlice_1....");
il.Emit(OpCodes.Call, typeof(Helpers).GetMethod("checkSlice")); // This works as expected, with mem = ReadOnlyMemory<byte>[16], start = 0, end = 4
il.Emit(OpCodes.Pop);

il.Emit(OpCodes.Ldarg, 0);
il.Emit(OpCodes.Ldc_I4, 0);
il.Emit(OpCodes.Ldc_I4, 4);
il.EmitWriteLine("checkSlice_2....");
il.Emit(OpCodes.Call, typeof(Helpers).GetMethod("checkSlice")); // This gets corrupted, with start = 4, end = random value and mem not being readable

// Print the result
il.Emit(OpCodes.Call, typeof(ReadOnlyMemory<byte>).GetMethod("ToArray"));
il.Emit(OpCodes.Call, typeof(BitConverter).GetMethod("ToString", new Type[] { typeof(byte[]) }));
il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));

il.Emit(OpCodes.Ldarga_S, 0);
il.Emit(OpCodes.Ldc_I4, 0);
il.Emit(OpCodes.Ldc_I4, 4);
il.EmitWriteLine("slice_1....");

il.Emit(OpCodes.Call, typeof(ReadOnlyMemory<byte>).GetMethod("Slice", new Type[2] {typeof(int), typeof(int)})); // throws ArgumentOutOfRangeException

// Print the result
il.Emit(OpCodes.Call, typeof(ReadOnlyMemory<byte>).GetMethod("ToArray"));
il.Emit(OpCodes.Call, typeof(BitConverter).GetMethod("ToString", new Type[] { typeof(byte[]) }));
il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));

il.Emit(OpCodes.Ret);


var del = m.CreateDelegate<Action<ReadOnlyMemory<byte>>>();
var s = new byte[] {
    3,0,0,0,
    0xAA,0xBB,0,0,
    0xCC,0xDD,0,0,
    0xEE,0xFF,0,0,
};
ReadOnlyMemory<byte> buff = new(s);
del(buff);

class Helpers
{
    public static ReadOnlyMemory<byte> checkSlice(ReadOnlyMemory<byte> mem, int start, int end)
    {
        
        try
        {
            Console.WriteLine($"{start}:{end}, {mem}");
            return mem.Slice(start, end);
        }
        catch (Exception)
        {
            Console.WriteLine("Exception on slice...." + start + ":" + end);
            return new ReadOnlyMemory<byte>();
        }
        
    } 
}

Here is the actual output:

checkSlice_1....
0:4, System.ReadOnlyMemory<Byte>[16]
checkSlice_2....
Exception on slice....4:1616289928
slice_1....
Unhandled exception. System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter 'start')
at EMITD_arraydeser_System.Int32[](ReadOnlyMemory`1)
at Program.<Main>$(String[] args) in C:\PATH\Program.cs:line 49

I suspect it has something to do with the 3 lines I use to print the ReadOnlyMemory (under the "// Print the result"-comment) i get returned from the slice (mb because ReadOnlyMemory is a valuetype?), but I don't understand how that influences the function call that was performed previously. Sadly, as far as I know there is no way to see the actual IL that was generated.

I have only a very limited understanding of IL, and I find the reference quite hard to understand, so I was hoping that someone smarter than me can explain how that (doesn't) work. :)


Solution

  • Because ReadOnlyMemory<T> is a value type, instance methods like ToArray require that you pass the address of the ReadOnlyMemory<T> instance, not the instance itself. To do this, use the Stloc and Ldloca opcodes with the local variable that you've declared:

    var il = m.GetILGenerator();
    var local = il.DeclareLocal(typeof(ReadOnlyMemory<byte>)); // Save the LocalBuilder
    
    il.Emit(OpCodes.Ldarg, 0);
    il.Emit(OpCodes.Ldc_I4, 0);
    il.Emit(OpCodes.Ldc_I4, 4);
    il.Emit(OpCodes.Call, typeof(Helpers).GetMethod("checkSlice"));
    il.Emit(OpCodes.Stloc, local); // Save the ReadOnlyMemory<byte> instance to the local variable
    
    // Print the result
    il.Emit(OpCodes.Ldloca, local); // IMPORTANT: Push the address of the local variable
    il.Emit(OpCodes.Call, typeof(ReadOnlyMemory<byte>).GetMethod("ToArray"));
    il.Emit(OpCodes.Call, typeof(BitConverter).GetMethod("ToString", new Type[] { typeof(byte[]) }));
    il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
    
    il.Emit(OpCodes.Ret);
    

    Your original code (which passes the result of checkSlice directly to ReadOnlyMemory<T>.ToArray) causes ToArray to access an invalid memory address, resulting in undefined behavior.