Skip to content

Skip hook MetadataCache::GetTypeInfoFromTypeDefinitionIndex if detected HybridCLR#260

Open
psyche314 wants to merge 1 commit intoBepInEx:masterfrom
psyche314:hybridclr
Open

Skip hook MetadataCache::GetTypeInfoFromTypeDefinitionIndex if detected HybridCLR#260
psyche314 wants to merge 1 commit intoBepInEx:masterfrom
psyche314:hybridclr

Conversation

@psyche314
Copy link
Copy Markdown

@psyche314 psyche314 commented Apr 5, 2026

Il2CppInterop crashes in games that use HybridCLR.

Game download / Web version link: Who is the undercover? (谁是卧底)

console

[16:43:01.048] [Il2CppInterop] Class::Init signatures have been exhausted, using a substitute!
Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at Il2CppInterop.Runtime.Injection.Hooks.MetadataCache_GetTypeInfoFromTypeDefinitionIndex_Hook.Hook(Int32)
   at Il2CppInterop.Runtime.Injection.Hooks.Class_FromName_Hook.Hook(Il2CppInterop.Runtime.Runtime.Il2CppImage*, IntPtr, IntPtr)
   at Il2CppInterop.Runtime.IL2CPP.il2cpp_class_from_name(IntPtr, System.String, System.String)
   at Il2CppInterop.Runtime.IL2CPP.GetIl2CppClass(System.String, System.String, System.String)
   at Il2CppSystem.Void..cctor()
   at System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(System.RuntimeTypeHandle)
   at Il2CppInterop.Runtime.Il2CppClassPointerStore`1[[Il2CppSystem.Void, Il2Cppmscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null]]..cctor()
   at Il2CppInterop.Runtime.Injection.ClassInjector.ConvertStaticMethod(VoidCtorDelegate, System.String, Il2CppInterop.Runtime.Runtime.VersionSpecific.Class.INativeClassStruct)
   at DynamicClass.DMD<Il2CppInterop.Runtime.Injection.ClassInjector::RegisterTypeInIl2Cpp>(System.Type, Il2CppInterop.Runtime.Injection.RegisterTypeOptions)
   at Il2CppInterop.Runtime.Injection.ClassInjector.RegisterTypeInIl2Cpp[[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](Il2CppInterop.Runtime.Injection.RegisterTypeOptions)
   at MelonLoader.Support.MonoEnumeratorWrapper.Register()
   at MelonLoader.Support.Main.Initialize(MelonLoader.ISupportModule_From)
   at System.RuntimeMethodHandle.InvokeMethod(System.Object, System.Span`1<System.Object> ByRef, System.Signature, Boolean, Boolean)
   at System.Reflection.RuntimeMethodInfo.Invoke(System.Object, System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo)
   at System.Reflection.MethodBase.Invoke(System.Object, System.Object[])
   at MelonLoader.SupportModule.LoadInterface(System.String)
   at MelonLoader.SupportModule.Setup()
   at MelonLoader.Core.Start()
   at MelonLoader.InternalUtils.BootstrapInterop.Start()

HybridCLR is a well-known hot-reload framework for Unity. It modifies the IL2CPP source code to support hot-reloading.

in Il2CppInterop source code, FindGetTypeInfoFromTypeDefinitionIndex attempts to locate il2cpp::vm::GlobalMetadata::GetTypeInfoFromTypeDefinitionIndex() by binary signature scanning. However, because HybridCLR inserts its own instrumentation at the beginning of this function, the signature no longer matches. As a result, the scan resolves an incorrect address, and hooking it causes a crash.

This PR adds a check for the presence of HybridCLR.RuntimeApi to determine whether the game uses HybridCLR. If HybridCLR is detected, this hook is skipped to prevent the crash.

libil2cpp/vm/GlobalMetadata.cpp

Il2CppClass* il2cpp::vm::GlobalMetadata::GetTypeInfoFromTypeDefinitionIndex(TypeDefinitionIndex index)
{
    // Code added by HybridCLR
    if (hybridclr::metadata::IsInterpreterIndex(index))
    {
        return hybridclr::metadata::MetadataModule::GetTypeInfoFromTypeDefinitionEncodeIndex(index);
    }
    if (index == kTypeIndexInvalid)
        return NULL;

    IL2CPP_ASSERT(index >= 0 && static_cast<uint32_t>(index) < s_GlobalMetadataHeader->typeDefinitionsSize / sizeof(Il2CppTypeDefinition));
    return utils::InitOnce(&s_TypeInfoDefinitionTable[index], &il2cpp::vm::g_MetadataLock, [index](il2cpp::os::FastAutoLock& _) { return FromTypeDefinition(index); });
}

Il2CppInterop.Runtime/Injection/Hooks/MetadataCache_GetTypeInfoFromTypeDefinitionIndex_Hook.cs

private IntPtr FindGetTypeInfoFromTypeDefinitionIndex(bool forceICallMethod = false)
{
    IntPtr getTypeInfoFromTypeDefinitionIndex = IntPtr.Zero;

    // il2cpp_image_get_class is added in 2018.3.0f1
    if (Il2CppInteropRuntime.Instance.UnityVersion < new Version(2018, 3, 0) || forceICallMethod)
    {
        // (Kasuromi): RuntimeHelpers.InitializeArray calls an il2cpp icall, proxy function does some magic before it invokes it
        // https://github.com/Unity-Technologies/mono/blob/unity-2018.2/mcs/class/corlib/System.Runtime.CompilerServices/RuntimeHelpers.cs#L53-L54
        IntPtr runtimeHelpersInitializeArray = InjectorHelpers.GetIl2CppMethodPointer(
            typeof(Il2CppSystem.Runtime.CompilerServices.RuntimeHelpers)
                .GetMethod("InitializeArray", new Type[] { typeof(Il2CppSystem.Array), typeof(IntPtr) })
        );
        Logger.Instance.LogTrace("Il2CppSystem.Runtime.CompilerServices.RuntimeHelpers::InitializeArray: 0x{RuntimeHelpersInitializeArrayAddress}", runtimeHelpersInitializeArray.ToInt64().ToString("X2"));

        var runtimeHelpersInitializeArrayICall = XrefScannerLowLevel.JumpTargets(runtimeHelpersInitializeArray).Last();
        if (XrefScannerLowLevel.JumpTargets(runtimeHelpersInitializeArrayICall).Count() == 1)
        {
            // is a thunk function
            Logger.Instance.LogTrace("RuntimeHelpers::thunk_InitializeArray: 0x{RuntimeHelpersInitializeArrayICallAddress}", runtimeHelpersInitializeArrayICall.ToInt64().ToString("X2"));
            runtimeHelpersInitializeArrayICall = XrefScannerLowLevel.JumpTargets(runtimeHelpersInitializeArrayICall).Single();
        }

        Logger.Instance.LogTrace("RuntimeHelpers::InitializeArray: 0x{RuntimeHelpersInitializeArrayICallAddress}", runtimeHelpersInitializeArrayICall.ToInt64().ToString("X2"));

        var typeGetUnderlyingType = XrefScannerLowLevel.JumpTargets(runtimeHelpersInitializeArrayICall).ElementAt(1);
        Logger.Instance.LogTrace("Type::GetUnderlyingType: 0x{TypeGetUnderlyingTypeAddress}", typeGetUnderlyingType.ToInt64().ToString("X2"));

        getTypeInfoFromTypeDefinitionIndex = XrefScannerLowLevel.JumpTargets(typeGetUnderlyingType).First();
    }
    else
    {
        var imageGetClassAPI = InjectorHelpers.GetIl2CppExport(nameof(IL2CPP.il2cpp_image_get_class));
        Logger.Instance.LogTrace("il2cpp_image_get_class: 0x{ImageGetClassApiAddress}", imageGetClassAPI.ToInt64().ToString("X2"));

        var imageGetType = XrefScannerLowLevel.JumpTargets(imageGetClassAPI).First();
        Logger.Instance.LogTrace("Image::GetType: 0x{ImageGetTypeAddress}", imageGetType.ToInt64().ToString("X2"));

        var imageGetTypeXrefs = XrefScannerLowLevel.JumpTargets(imageGetType).ToArray();

        if (imageGetTypeXrefs.Length == 0)
        {
            // (Kasuromi): Image::GetType appears to be inlined in il2cpp_image_get_class on some occasions,
            // if the unconditional xrefs are 0 then we are in the correct method (seen on unity 2019.3.15)
            getTypeInfoFromTypeDefinitionIndex = imageGetType;
        }
        else getTypeInfoFromTypeDefinitionIndex = imageGetTypeXrefs[0];
        if ((getTypeInfoFromTypeDefinitionIndex.ToInt64() & 0xF) != 0)
        {
            Logger.Instance.LogTrace("Image::GetType xref wasn't aligned, attempting to resolve from icall");
            return FindGetTypeInfoFromTypeDefinitionIndex(true);
        }
        if (imageGetTypeXrefs.Count() > 1 && UnityVersionHandler.IsMetadataV29OrHigher)
        {
            // (Kasuromi): metadata v29 introduces handles and adds extra calls, a check for unity versions might be necessary in the future

            Logger.Instance.LogTrace($"imageGetTypeXrefs.Length: {imageGetTypeXrefs.Length}");

            // If the game is built as IL2CPP Master, GetAssemblyTypeHandle is inlined, xrefs length is 3 and it's the first function call,
            // if not, it's the last call.
            var getTypeInfoFromHandle = imageGetTypeXrefs.Length == 2 ? imageGetTypeXrefs.Last() : imageGetTypeXrefs.First();

            Logger.Instance.LogTrace($"getTypeInfoFromHandle: {getTypeInfoFromHandle:X2}");

            var getTypeInfoFromHandleXrefs = XrefScannerLowLevel.JumpTargets(getTypeInfoFromHandle).ToArray();

            // If getTypeInfoFromHandle xrefs is not a single call, it's the function we want, if not, we keep xrefing until we find it
            if (getTypeInfoFromHandleXrefs.Length != 1)
            {
                getTypeInfoFromTypeDefinitionIndex = getTypeInfoFromHandle;
                Logger.Instance.LogTrace($"Xrefs length was not 1, getTypeInfoFromTypeDefinitionIndex: {getTypeInfoFromTypeDefinitionIndex:X2}");
            }
            else
            {
                // Two calls, second one (GetIndexForTypeDefinitionInternal) is inlined
                getTypeInfoFromTypeDefinitionIndex = getTypeInfoFromHandleXrefs.Single();
                // Xref scanner is sometimes confused about getTypeInfoFromHandle so we walk all the thunks until we hit the big method we need
                while (XrefScannerLowLevel.JumpTargets(getTypeInfoFromTypeDefinitionIndex).ToArray().Length == 1)
                {
                    getTypeInfoFromTypeDefinitionIndex = XrefScannerLowLevel.JumpTargets(getTypeInfoFromTypeDefinitionIndex).Single();
                }
            }
        }
    }

    return getTypeInfoFromTypeDefinitionIndex;
}

@psyche314
Copy link
Copy Markdown
Author

This is similar to #251, but this approach is more conservative to prevent potential cross-platform issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant