Zeroed data on ref struct parameter after patching
Unity Version: 2022.3.28f1 (IL2CPP) BepInEx Version: Both 6.0.0-be.733 and 6.0.0-pre.2 Game : Suikoden I&II HD Remaster
Using the simple following code :
[HarmonyPatch(typeof(EVENTCON), nameof(EVENTCON.EventAtariCheckFunc1))]
[HarmonyPostfix]
static void CheckAtari(ref RECT rec)
{
Plugin.Log.LogWarning($"x={rec.x} y={rec.y} w={rec.w} h={rec.h}");
}
Or even with the simpler code :
[HarmonyPatch(typeof(EVENTCON), nameof(EVENTCON.EventAtariCheckFunc1))]
[HarmonyPrefix]
static void CheckAtari()
{
}
To patch this function :
private void EventAtariCheckFunc1(int pno, int dir, ref RECT rec)
It results in the "ref RECT rec" always returning a rec with zeroed data. I had to check with a debugger (x96dbg).
In the console I get this info with the patch :
x=512 y=768 w=0 h=0
The assembly code at the call site for EventAtariCheckFunc1 :
Result without the patch :
As you can see the data for the rec which is on the stack, is not zeroed at the address pointed by the R9 register. 0x300 (=768) corresponds to the y value of rec and 0x200 (=512) corresponds to the x value of the rec.
Result with the patch :
Now the data for rec is zeroed !!! The hook displayed the data of rec properly, so its data is lost at some point after the Postfix.
Decompiled RECT:
[StructLayout(LayoutKind.Explicit)]
public struct RECT
{
private static readonly System.IntPtr NativeFieldInfoPtr_x;
private static readonly System.IntPtr NativeFieldInfoPtr_y;
private static readonly System.IntPtr NativeFieldInfoPtr_w;
private static readonly System.IntPtr NativeFieldInfoPtr_h;
private static readonly System.IntPtr NativeMethodInfoPtr__ctor_Public_Void_Int32_Int32_Int32_Int32_0;
[FieldOffset(0)]
public int x;
[FieldOffset(4)]
public int y;
[FieldOffset(8)]
public int w;
[FieldOffset(12)]
public int h;
[CallerCount(28)]
[CachedScanResults(RefRangeStart = 739593, RefRangeEnd = 739621, XrefRangeStart = 739593, XrefRangeEnd = 739593, MetadataInitTokenRva = 0L, MetadataInitFlagRva = 0L)]
public unsafe RECT([DefaultParameterValue(null)] int _x, [DefaultParameterValue(null)] int _y, [DefaultParameterValue(null)] int _w, [DefaultParameterValue(null)] int _h)
{
System.IntPtr* ptr = stackalloc System.IntPtr[4];
*ptr = (nint)(&_x);
*(int**)((byte*)ptr + checked((nuint)1u * unchecked((nuint)sizeof(System.IntPtr)))) = &_y;
*(int**)((byte*)ptr + checked((nuint)2u * unchecked((nuint)sizeof(System.IntPtr)))) = &_w;
*(int**)((byte*)ptr + checked((nuint)3u * unchecked((nuint)sizeof(System.IntPtr)))) = &_h;
System.Runtime.CompilerServices.Unsafe.SkipInit(out System.IntPtr exc);
System.IntPtr intPtr = IL2CPP.il2cpp_runtime_invoke(NativeMethodInfoPtr__ctor_Public_Void_Int32_Int32_Int32_Int32_0, (nint)System.Runtime.CompilerServices.Unsafe.AsPointer(ref this), (void**)ptr, ref exc);
Il2CppException.RaiseExceptionIfNecessary(exc);
}
static RECT()
{
Il2CppClassPointerStore<RECT>.NativeClassPtr = IL2CPP.GetIl2CppClass("GSDShare.dll", "", "RECT");
IL2CPP.il2cpp_runtime_class_init(Il2CppClassPointerStore<RECT>.NativeClassPtr);
NativeFieldInfoPtr_x = IL2CPP.GetIl2CppField(Il2CppClassPointerStore<RECT>.NativeClassPtr, "x");
NativeFieldInfoPtr_y = IL2CPP.GetIl2CppField(Il2CppClassPointerStore<RECT>.NativeClassPtr, "y");
NativeFieldInfoPtr_w = IL2CPP.GetIl2CppField(Il2CppClassPointerStore<RECT>.NativeClassPtr, "w");
NativeFieldInfoPtr_h = IL2CPP.GetIl2CppField(Il2CppClassPointerStore<RECT>.NativeClassPtr, "h");
NativeMethodInfoPtr__ctor_Public_Void_Int32_Int32_Int32_Int32_0 = IL2CPP.GetIl2CppMethodByToken(Il2CppClassPointerStore<RECT>.NativeClassPtr, 100663932);
}
public unsafe Il2CppSystem.Object BoxIl2CppObject()
{
return new Il2CppSystem.Object(IL2CPP.il2cpp_value_box(Il2CppClassPointerStore<RECT>.NativeClassPtr, (nint)System.Runtime.CompilerServices.Unsafe.AsPointer(ref this)));
}
}
Decompiled EVENTCON :
[CallerCount(17)]
[CachedScanResults(RefRangeStart = 105008, RefRangeEnd = 105025, XrefRangeStart = 104988, XrefRangeEnd = 105008, MetadataInitTokenRva = 0L, MetadataInitFlagRva = 0L)]
public unsafe void EventAtariCheckFunc1([DefaultParameterValue(null)] int pno, [DefaultParameterValue(null)] int dir, [DefaultParameterValue(null)] ref RECT rec)
{
IL2CPP.Il2CppObjectBaseToPtrNotNull(this);
System.IntPtr* ptr = stackalloc System.IntPtr[3];
*ptr = (nint)(&pno);
*(int**)((byte*)ptr + checked((nuint)1u * unchecked((nuint)sizeof(System.IntPtr)))) = &dir;
*(void**)((byte*)ptr + checked((nuint)2u * unchecked((nuint)sizeof(System.IntPtr)))) = System.Runtime.CompilerServices.Unsafe.AsPointer(ref rec);
System.Runtime.CompilerServices.Unsafe.SkipInit(out System.IntPtr exc);
System.IntPtr intPtr = IL2CPP.il2cpp_runtime_invoke(NativeMethodInfoPtr_EventAtariCheckFunc1_Private_Void_Int32_Int32_byref_RECT_0, IL2CPP.Il2CppObjectBaseToPtrNotNull(this), (void**)ptr, ref exc);
Il2CppInterop.Runtime.Il2CppException.RaiseExceptionIfNecessary(exc);
}
Can the issue be reproduced with a Mono game?
I just tried to reproduce the issue with a mono game (Tales of Graces f Remastered).
I added the following struct and class with DnSpyEx to Assembly-CSharp.dll :
using System;
using System.IO;
namespace Test;
public struct RECT
{
public int x;
public int y;
public int w;
public int h;
}
public class RectTest
{
public static void Test()
{
RECT rec;
rec.x = 0;
rec.y = 0;
rec.w = 0;
rec.h = 0;
EventAtariCheckFunc1(1, 2, ref rec);
using (StreamWriter writer = new StreamWriter("d:\\test.txt"))
{
writer.WriteLine($"x={rec.x} y={rec.y} w={rec.w} h={rec.h}");
}
}
private static void EventAtariCheckFunc1(int pno, int dir, ref RECT rec)
{
rec.x = 512;
rec.y = 768;
}
}
I changed to body of the method FrameRateManager .SetTargetFramerate() to call the RectTest.Test() function.
Then I used an existing bepinex plugin (TalesOfGracesFFix) and added the following patch :
[HarmonyPatch]
public class TestPatches
{
[HarmonyPatch(typeof(Test.RectTest), nameof(Test.RectTest.EventAtariCheckFunc1))]
[HarmonyPrefix]
static void EventAtariCheckFunc1()
{
Log.LogWarning("HELLO");
}
}
The result is that I can't reproduce the bug :
- the file d:\test.txt contains the correct values for the RECT.
- HELLO is displayed in the bepinex logs, showing that the method EventAtariCheckFunc1 was indeed patched.
If you want to reproduce the bug with Suikoden I&II HD Remaster, you can use my plugin and add this code in the class ResetGamePatch of Patches\ResetApplication.cs :
[HarmonyPatch(typeof(GSD2.EVENTCON), nameof(GSD2.EVENTCON.EventAtariCheckFunc1))]
[HarmonyPrefix]
static void CheckAtari()
{
}
The function EventAtariCheckFunc1 is called when you move in Suikoden 2, it checks for any collision. When the rec has zeroed values, the player cannot move so it's easy to notice.