Skip to main content

Intro

Edge Legacy’s XAML WebView control has always been incompatible with both unpackaged apps and legacy Win32 apps. Any attempt to using this control under these conditions will always result in an exception, whether on XAML Islands or any other non-UWP XAML hosting techniques such as XamlHost or XamlPresenter. I wanted to use WebView as fallback SVG renderer for MrmTool since the default NanoSVG-based renderer is limited and doesn’t cover all of SVG features, but the problem is that MrmTool is an unpackaged Win32 app so it cannot normally use the WebView control, and I wanted to avoid WebView2 since it would increase the app size and it’s not available (or at least not installed by default) on all platform environments that MrmTool supports. So I decided to investigate why it throws exceptions and see if I can fix it or at least patch it to work…

Investigating

The first exception we face seems to be E_XAMLPARSEFAILED img But that error is from the XAML parser so it’s already too late and the actual error was absorbed by the parser, so let’s initialize WebView early manually and see what we get… img Ah E_FAIL that makes more sense, but sadly it seems like the actual location of the exception has been lost, the callstack is from the generic XAML HRESULT handler, so lets see if we can track where that HRESULT was originally returned… The easiest way to do that would be following the WebView initialization code under IDA Pro and seeing where that HRESULT first occurs. it seems like the init logic happens in CWebView::CreateComponent which then calls DirectUI::WebView::CreateComponent which then calls DirectUI::CoreWebViewHost::CreateComponent, and here we found our first occurrence of E_FAIL (0x80004005): img It seems to be returned if the global field s_bWebPlatformSecurityManagerFactoryCallbackRegistered is false, so lets see Xrefs of this field to see what sets it img As you can see, it seems to be set by a function called DirectUI::CoreWebViewHost::RegisterWebViewPermanentSecurityManager img I put a breakpoint on this function to see what goes wrong in it but the breakpoint was never hit?? So there must be something else preventing this function from being called, lets see what calls this function and how it is called… img It seems to be called by EnsureDelayedInit so lets check it out… img Ah so it’s indeed called conditionally based on DesignerInterop::GetDesignerMode, DirectUI::XamlRuntime::IsWebViewEnabled, and ShouldProcessRegisterWebViewSecurityManager. We are not in the designer so we can ignore the first check, and by debugging I found out that the second check returns true so we can ignore that one too, so it must be the third one, so lets check it out… img By looking at this function it seems like for it to return true one of these conditions must be true:
  • The application is running under AppContainer
  • The application is not the Settings app and the application is not using ClassicDesktop AppModel windowing policy
As expected both are false in our case, the second condition seems to be the easier one to patch since we only need to patch the return value of the AppPolicyGetWindowingModel function.

Patching & More Investigation

The most logical thing to do would be hooking the function using Detours (or similar hooking libraries), but since MrmTool is written in C# it would be harder to statically link Detours without sarcrificing CoreCLR and being exclusively on NativeAOT, and using Detours as a dynamic library would increase the app size and the number of files it carries, so I decided to use IAT patching instead. Luckily a project I’m part of called XWine1 already had helpers so I ported them to C# with few adaptations to unpatch the functions on process exit to accommodate for GC Shutdown:
internal static readonly Dictionary<nuint, nuint> PatchedFunctions = [];

// Originally written by @DaZombieKiller for the XWine1 project
private static HRESULT XWineFindImport(HMODULE Module,
                                       byte* Import,
                                       IMAGE_THUNK_DATA* pImportAddressTable,
                                       IMAGE_THUNK_DATA* pImportNameTable,
                                       IMAGE_THUNK_DATA** pThunk)
{
    for (nuint j = 0; pImportNameTable[j].u1.AddressOfData > 0; j++)
    {
        if ((pImportNameTable[j].u1.AddressOfData & IMAGE.IMAGE_ORDINAL_FLAG) != 0)
        {
            if (!IS_INTRESOURCE((nuint)Import))
                continue;

            if (((pImportNameTable[j].u1.Ordinal & ~IMAGE.IMAGE_ORDINAL_FLAG) == (nuint)Import))
            {
                *pThunk = &pImportAddressTable[j];
                return S.S_OK;
            }

            continue;
        }

        var name = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)Unsafe.AsPointer(in ((IMAGE_IMPORT_BY_NAME*)((byte*)Module + pImportNameTable[j].u1.AddressOfData))->Name.e0));
        var importName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(Import);

        if (!name.SequenceEqual(importName))
            continue;

        *pThunk = &pImportAddressTable[j];
        return S.S_OK;
    }

    *pThunk = null;
    return E.E_FAIL;
}

// Originally written by @DaZombieKiller for the XWine1 project
internal static HRESULT XWineGetImport(HMODULE Module,
                                       HMODULE ImportModule,
                                       byte* Import,
                                       IMAGE_THUNK_DATA** pThunk)
{
    if (ImportModule.Value is null)
        return E.E_INVALIDARG;

    if (pThunk == null)
        return E.E_POINTER;

    if (Module.Value is null)
        Module = GetModuleHandleW(null);

    var dosHeader = (IMAGE_DOS_HEADER*)Module;
    var ntHeaders = (IMAGE_NT_HEADERS*)((byte*)Module + dosHeader->e_lfanew);
    var directory = &ntHeaders->OptionalHeader.DataDirectory[IMAGE.IMAGE_DIRECTORY_ENTRY_IMPORT];

    if (directory->VirtualAddress <= 0 || directory->Size <= 0)
        return E.E_FAIL;

    var peImports = (IMAGE_IMPORT_DESCRIPTOR*)((byte*)Module + directory->VirtualAddress);

    for (nuint i = 0; peImports[i].Name > 0; i++)
    {
        if (GetModuleHandleA((sbyte*)((byte*)Module + peImports[i].Name)) != ImportModule)
            continue;

        var iatThunks = (IMAGE_THUNK_DATA*)((byte*)Module + peImports[i].FirstThunk);
        var intThunks = (IMAGE_THUNK_DATA*)((byte*)Module + peImports[i].OriginalFirstThunk);

        if (SUCCEEDED(XWineFindImport(Module, Import, iatThunks, intThunks, pThunk)))
            return S.S_OK;
    }

    var delayDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE.IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT];
    if (delayDir->VirtualAddress > 0 && directory->Size > 0)
    {
        var delayImports = (IMAGE_DELAYLOAD_DESCRIPTOR*)((byte*)Module + delayDir->VirtualAddress);

        for (nuint i = 0; delayImports[i].DllNameRVA > 0; i++)
        {
            if (GetModuleHandleA((sbyte*)((byte*)Module + delayImports[i].DllNameRVA)) != ImportModule)
                continue;

            var iatThunks = (IMAGE_THUNK_DATA*)((byte*)Module + delayImports[i].ImportAddressTableRVA);
            var intThunks = (IMAGE_THUNK_DATA*)((byte*)Module + delayImports[i].ImportNameTableRVA);

            if (SUCCEEDED(XWineFindImport(Module, Import, iatThunks, intThunks, pThunk)))
                return S.S_OK;
        }
    }

    *pThunk = null;
    return E.E_FAIL;
}

// Originally written by @DaZombieKiller for the XWine1 project
internal static HRESULT XWinePatchImport(HMODULE Module,
                                         HMODULE ImportModule,
                                         byte* Import,
                                         void* Function)
{
    HRESULT hr;

    uint protect;
    IMAGE_THUNK_DATA* pThunk;
    if (!SUCCEEDED_LOG(hr = XWineGetImport(Module, ImportModule, Import, &pThunk)))
    {
        return hr;
    }

    if (!VirtualProtect(&pThunk->u1.Function, (nuint)sizeof(nuint), PAGE.PAGE_READWRITE, &protect))
    {
        return HRESULT_FROM_WIN32(GetLastError());
    }

    nuint originalFunction = (nuint)pThunk->u1.Function;
    pThunk->u1.Function = (nuint)Function;

    PatchedFunctions.TryAdd((nuint)(void*)&pThunk->u1.Function, originalFunction);


    if (!VirtualProtect(&pThunk->u1.Function, (nuint)sizeof(nuint), protect, &protect))
    {
        return HRESULT_FROM_WIN32(GetLastError());
    }

    return S.S_OK;
}
I didn’t need few of the features this helper includes so I removed them for performance reasons:
internal static readonly (nuint Thunk, nuint OriginalFunction)[] PatchedFunctions = new (nuint, nuint)[Constants.PatchesCount];

// Originally written by @DaZombieKiller for the XWine1 project
private static HRESULT XWineFindImport(HMODULE Module,
                                       ReadOnlySpan<byte> Import,
                                       IMAGE_THUNK_DATA* pImportAddressTable,
                                       IMAGE_THUNK_DATA* pImportNameTable,
                                       IMAGE_THUNK_DATA** pThunk)
{
    for (nuint j = 0; pImportNameTable[j].u1.AddressOfData > 0; j++)
    {
        if ((pImportNameTable[j].u1.AddressOfData & IMAGE.IMAGE_ORDINAL_FLAG) != 0)
            continue;

        var name = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)Unsafe.AsPointer(in ((IMAGE_IMPORT_BY_NAME*)((byte*)Module + pImportNameTable[j].u1.AddressOfData))->Name.e0));

        if (!name.SequenceEqual(Import))
            continue;

        *pThunk = &pImportAddressTable[j];
        return S.S_OK;
    }

    *pThunk = null;
    return E.E_FAIL;
}

// Originally written by @DaZombieKiller for the XWine1 project
internal static HRESULT XWineGetImport(HMODULE Module,
                                       HMODULE ImportModule,
                                       ReadOnlySpan<byte> Import,
                                       IMAGE_THUNK_DATA** pThunk)
{
    if (ImportModule.Value is null)
        return E.E_INVALIDARG;

    if (pThunk == null)
        return E.E_POINTER;

    if (Module.Value is null)
        Module = GetModuleHandleW(null);

    var dosHeader = (IMAGE_DOS_HEADER*)Module;
    var ntHeaders = (IMAGE_NT_HEADERS*)((byte*)Module + dosHeader->e_lfanew);
    var directory = &ntHeaders->OptionalHeader.DataDirectory[IMAGE.IMAGE_DIRECTORY_ENTRY_IMPORT];

    if (directory->VirtualAddress <= 0 || directory->Size <= 0)
        return E.E_FAIL;

    var peImports = (IMAGE_IMPORT_DESCRIPTOR*)((byte*)Module + directory->VirtualAddress);

    for (nuint i = 0; peImports[i].Name > 0; i++)
    {
        if (GetModuleHandleA((sbyte*)((byte*)Module + peImports[i].Name)) != ImportModule)
            continue;

        var iatThunks = (IMAGE_THUNK_DATA*)((byte*)Module + peImports[i].FirstThunk);
        var intThunks = (IMAGE_THUNK_DATA*)((byte*)Module + peImports[i].OriginalFirstThunk);

        if (SUCCEEDED(XWineFindImport(Module, Import, iatThunks, intThunks, pThunk)))
            return S.S_OK;
    }

    var delayDir = &ntHeaders->OptionalHeader.DataDirectory[IMAGE.IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT];
    if (delayDir->VirtualAddress > 0 && directory->Size > 0)
    {
        var delayImports = (IMAGE_DELAYLOAD_DESCRIPTOR*)((byte*)Module + delayDir->VirtualAddress);

        for (nuint i = 0; delayImports[i].DllNameRVA > 0; i++)
        {
            if (GetModuleHandleA((sbyte*)((byte*)Module + delayImports[i].DllNameRVA)) != ImportModule)
                continue;

            var iatThunks = (IMAGE_THUNK_DATA*)((byte*)Module + delayImports[i].ImportAddressTableRVA);
            var intThunks = (IMAGE_THUNK_DATA*)((byte*)Module + delayImports[i].ImportNameTableRVA);

            if (SUCCEEDED(XWineFindImport(Module, Import, iatThunks, intThunks, pThunk)))
                return S.S_OK;
        }
    }

    *pThunk = null;
    return E.E_FAIL;
}

// Originally written by @DaZombieKiller for the XWine1 project
internal static HRESULT XWinePatchImport(HMODULE Module,
                                         HMODULE ImportModule,
                                         ReadOnlySpan<byte> Import,
                                         void* Function,
                                         int Index)
{
    HRESULT hr;

    uint protect;
    IMAGE_THUNK_DATA* pThunk;
    if (!SUCCEEDED_LOG(hr = XWineGetImport(Module, ImportModule, Import, &pThunk)))
    {
        return hr;
    }

    if (!VirtualProtect(&pThunk->u1.Function, (nuint)sizeof(nuint), PAGE.PAGE_READWRITE, &protect))
    {
        return HRESULT_FROM_WIN32(GetLastError());
    }

    nuint originalFunction = (nuint)pThunk->u1.Function;
    pThunk->u1.Function = (nuint)Function;

    PatchedFunctions[Index] = ((nuint)(void*)&pThunk->u1.Function, originalFunction);

    if (!VirtualProtect(&pThunk->u1.Function, (nuint)sizeof(nuint), protect, &protect))
    {
        return HRESULT_FROM_WIN32(GetLastError());
    }

    return S.S_OK;
}
Then it was time to patch the AppPolicyGetWindowingModel function:
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static int AppPolicyGetWindowingModelHook(HANDLE processToken, AppPolicyWindowingModel* policy)
{
    *policy = AppPolicyWindowingModel.AppPolicyWindowingModel_None;
    return TerraFX.Interop.Windows.ERROR.ERROR_SUCCESS;
}

internal static bool PatchWebViewAppModelChecks()
{
    var appmodel = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernel.appcore.dll"u8)));
    var xaml = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("Windows.UI.Xaml.dll"u8)));

    if (appmodel.Value is not null && xaml.Value is not null)
    {
        var fptr = (delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, int>)&AppPolicyGetWindowingModelHook;
        if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "AppPolicyGetWindowingModel"u8, fptr, 0)))
        {
            AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
            return true;
        }
    }

    return false;
}

private static void OnProcessExit(object? sender, EventArgs e)
{
    foreach (var (Thunk, OriginalFunction) in PatchingHelper.PatchedFunctions)
    {
        var thunk = (nuint*)Thunk;

        uint protect;
        if (VirtualProtect(thunk, (nuint)sizeof(nuint), PAGE.PAGE_READWRITE, &protect))
        {
            *thunk = OriginalFunction;
            VirtualProtect(thunk, (nuint)sizeof(nuint), protect, &protect);
        }
    }
}
So did it fix the exception? Yes! So the WebView control worked? well, No! We got another exception!!! img This time we got APPMODEL_ERROR_NO_PACKAGE, so XAML must be calling some packaging related APIs in the WebView init code, lets see if we can find any… Hm this call in DirectUI::CoreWebViewHost::CreateComponent seems interesting, it mentions “Appx” so it must be doing something with packaging APIs: img So lets see what it does… img Judging by the function name and its code, it seems to be getting the current package info then uses that to register activation protocols the package registers for WebView to be able to invoke them, so it doesn’t seem to be a cruical thing for our use case, so lets see if we can redirect it to use another package that is always installed on the system for that, we can use the Settings app for example, so lets start by checking what functions are used to get the package info… img it’s using GetCurrentPackageInfo for that, so lets patch it and redirect it to the Settings app package…
private static readonly string? settingsPFN =
    new PackageManager().FindPackagesForUser(null, "windows.immersivecontrolpanel_cw5n1h2txyewy").FirstOrDefault()?.Id.FullName;

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static int GetCurrentPackageInfoHook(uint flags, uint* bufferLength, byte* buffer, uint* count)
{
    if (settingsPFN is not null)
    {
        fixed (char* pSettingPFN = settingsPFN)
        {
            PACKAGE_INFO_REFERENCE pir;
            if (OpenPackageInfoByFullName(pSettingPFN, 0, &pir) is TerraFX.Interop.Windows.ERROR.ERROR_SUCCESS)
            {
                var result = GetPackageInfo(pir, flags, bufferLength, buffer, count);
                _ = ClosePackageInfo(pir);
                return result;
            }
        }
    }

    return GetCurrentPackageInfo(flags, bufferLength, buffer, count);
}

internal static bool PatchWebViewAppModelChecks()
{
    var appmodel = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernel.appcore.dll"u8)));
    var xaml = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("Windows.UI.Xaml.dll"u8)));

    if (appmodel.Value is not null && xaml.Value is not null)
    {
        var fptr = (delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, int>)&AppPolicyGetWindowingModelHook;
        if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "AppPolicyGetWindowingModel"u8, fptr, 0)))
        {
            var fptr2 = (delegate* unmanaged[Stdcall]<uint, uint*, byte*, uint*, int>)&GetCurrentPackageInfoHook;
            if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "GetCurrentPackageInfo"u8, fptr2, 1)))
            {
                AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
                return true;
            }
        }
    }

    return false;
}
Now, lets see what that changed… img Hm, an interrupt from a function called Abandonment::InduceHRESULTAbandonment inside edgehtml.dll, lets check this function to see if we can get the HRESULT somehow… img It seems to be setting the HRESULT on a global field called Abandonment::LastError, lets check its address in Visual Studio disassembler so we can read it from the debugger… img Interesting, it’s E_ACCESSDENIED (-2147024891 = 0x80070005), and it’s thrown from CCoreWebViewTaskHandler::_CreateWebPlatform so lets see if we can find it in that function… img First call seems to be about getting user identity and it doesn’t seem to be the one that is failing judging by the disassembly at function offset shown in the exception callstack, so lets see the second usage… img It seems to be checking the result of the CWebPlatform::CreateInstance function call, so lets check this one… img And here’s our E_ACCESSDENIED! It seems like the execution never reaches this block or otherwise the HRESULT would have changed since this block changes it img So both checks of that if condition must be failing, and I was able to confim via debugger that they indeed are… The second check seems to be for checking the device family so lets ignore that one since it would also fail for UWP under the same device family, the first check seems to be checking some boolean state 536870925 / 0x2000000D using the IEConfiguration_GetBool function from iertutil.dll, so lets check what this state is… img It seems to be reading the state from global field unk_18029808A (name generated by IDA Pro), so lets see what sets that… It seems to be set through _IEProcessState_GetStateHolder function but it seems like this function is only called by IEConfiguration_SetBool function which isn’t called by that state ID in the dll, so unk_18029808A must be a part of a global struct field and it’s accessed through it instead, there seem to be a function called IEConfiguration_Initialize and judging by its name it seems to be the one responsible for initializing these state values, so lets check it out… img It seems to be calling another function _IEConfiguation_InitializeHelper with a global field byte_180298070 casted to struct SIEProcessState* passed as the second paramter, which is just 26 bytes away from unk_18029808A so this must be its parent struct, so lets see if anything sets [second parameter] + 26 in that function… img And there indeed is! It’s set to true if IEIsWebPlatformProcess returned true, so lets check that function… img the call to IEIsImmersiveProcess immediatly caught my attention, since that would definetly be true on UWP but false on legacy Win32, and comparing the results of all these checks with a UWP app under a debugger confirms that this indeed is the different result out of them, so lets check that function and see if we can patch it… img Hm, so it uses IsImmersiveProcess for that but it’s dynamically loaded so a direct IAT patch wouldn’t work, we need to IAT patch GetProcAddress first then from that we can return our custom IsImmersiveProcess, so lets do that!
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static int IsImmersiveProcessHook(void* unk)
{
    return 1;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static void* GetProcAddressHook(HMODULE module, sbyte* procName)
{
    if (procName is not null && !IS_INTRESOURCE((nuint)procName))
    {
        var name = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)procName);
        if (name.SequenceEqual("IsImmersiveProcess"u8))
        {
            return (delegate* unmanaged[Stdcall]<void*, int>)&IsImmersiveProcessHook;
        }
    }

    return GetProcAddress(module, procName);
}

internal static bool PatchWebViewAppModelChecks()
{
    var appmodel = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernel.appcore.dll"u8)));
    var xaml = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("Windows.UI.Xaml.dll"u8)));
    var iertutil = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("iertutil.dll"u8)));
    var kb = GetModuleHandleA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernelbase.dll"u8)));

    if (appmodel.Value is not null &&
        xaml.Value is not null &&
        iertutil.Value is not null)
    {
        var fptr = (delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, int>)&AppPolicyGetWindowingModelHook;
        if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "AppPolicyGetWindowingModel"u8, fptr, 0)))
        {
            var fptr2 = (delegate* unmanaged[Stdcall]<uint, uint*, byte*, uint*, int>)&GetCurrentPackageInfoHook;
            if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "GetCurrentPackageInfo"u8, fptr2, 1)))
            {
                var fptr3 = (delegate* unmanaged[Stdcall]<HMODULE, sbyte*, void*>)&GetProcAddressHook;
                if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(iertutil, kb, "GetProcAddress"u8, fptr3, 2)))
                {
                    AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
                    return true;
                }
            }
        }
    }

    return false;
}
Now, lets see what that changed… img It failfasts inside CreateUriPriv function, lets check the function and see what could cause that… img Hm, it failfasts if value for state ID 0x2000000F isn’t what it expects, which I assume is true, using the same method we used previously I found the global field (or struct member to be more accurate) for this state ID to be unk_18029808C which is 28 bytes away from byte_180298070, so lets see what sets this state ID in _IEConfiguation_InitializeHelper img it seems to be set to value of byte_180296648 which is set by InitOnceIsCurrentProcessEdgeContentHost, so lets check it out… img It seems to return true if the value of DWORD state ID 0x1000002D is 2, so lets see where that state ID is stored by checking IEConfiguration_GetDWORD img
img
img The function checks if the ID is 0x1000002D and if so then it sets v7 to RVA 0x1802987C0 then later sets v3 to *((DWORD*)v7 + 292) and then it returns v3, so (DWORD*)v7 + 292 must be the RVA where state value is stored, and it can be calculated like this: 0x1802987C0 + (sizeof(DWORD) * 292) = 0x1802987C0 + (4 * 292) = 0x180298c50, so lets check Xrefs of that RVA… img It seems to be set by IEConfiguration_SetBrowserAppProfile function, so lets check it out… img
img The function seems to be setting it to the value of the second parameter of the function,and this function seems to be exported at ordinal #797! So now we have to figure out the params of the function and call it from our app. The first parameter seems to be a string judging by the lstrcmpW call and calls to this function from other functions in the dll confirm this and it seems to be the selected profile judging by the function name, and we know that we need to set the second parameter to 2 for 0x2000000F to be true and it seems like this parameter is the type of the profile selected? since there seem to be other valid values, and the last paramter is unknown but judging by the call in IEConfiguration_SetBrowserAppProfileDefault it seems like it can be 0 img I checked Edge Legacy binaries and it seems to be calling this function with the first parameter being “MicrosoftEdge”, so lets go ahead and call this function…
[PreserveSig]
[DllImport("iertutil.dll", EntryPoint = "#797")]
private static extern HRESULT IEConfiguration_SetBrowserAppProfile(char* profile, uint type, uint unk);

internal static bool PatchWebViewAppModelChecks()
{
    var appmodel = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernel.appcore.dll"u8)));
    var xaml = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("Windows.UI.Xaml.dll"u8)));
    var iertutil = LoadLibraryA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("iertutil.dll"u8)));
    var kb = GetModuleHandleA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("kernelbase.dll"u8)));

    if (appmodel.Value is not null &&
        xaml.Value is not null &&
        iertutil.Value is not null)
    {
        var fptr = (delegate* unmanaged[Stdcall]<HANDLE, AppPolicyWindowingModel*, int>)&AppPolicyGetWindowingModelHook;
        if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "AppPolicyGetWindowingModel"u8, fptr, 0)))
        {
            var fptr2 = (delegate* unmanaged[Stdcall]<uint, uint*, byte*, uint*, int>)&GetCurrentPackageInfoHook;
            if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(xaml, appmodel, "GetCurrentPackageInfo"u8, fptr2, 1)))
            {
                var fptr3 = (delegate* unmanaged[Stdcall]<HMODULE, sbyte*, void*>)&GetProcAddressHook;
                if (SUCCEEDED_LOG(PatchingHelper.XWinePatchImport(iertutil, kb, "GetProcAddress"u8, fptr3, 2)) &&
                    SUCCEEDED_LOG(IEConfiguration_SetBrowserAppProfile((char*)Unsafe.AsPointer(in MemoryMarshal.GetReference("MicrosoftEdge".AsSpan())), 2, 0)))
                {
                    AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
                    return true;
                }
            }
        }
    }

    return false;
}

Results

Now, lets see if it finally works… Untitled4 IT WORKS!!! 🎉

Fixing a Crash on Process Shutdown

The app seems to throw an exception on shutdown after WebView is loaded img It seems like the edgehtml dll trying to call SubmitThreadpoolWork when detaching/unloading on process shutdown which isn’t allowed, but we can workaround this by forcing edgehtml.dll to cleanup early by sending it a DLL_PROCESS_DETACH signal inside our OnProcessExit event handler:
internal static void* GetModuleEntryPoint(HMODULE Module)
{
    if (Module.Value is null)
        return null;

    var dosHeader = (IMAGE_DOS_HEADER*)Module;
    var ntHeaders = (IMAGE_NT_HEADERS*)((byte*)Module + dosHeader->e_lfanew);

    return (byte*)Module + ntHeaders->OptionalHeader.AddressOfEntryPoint;
}

private static void OnProcessExit(object? sender, EventArgs e)
{
    foreach (var (Thunk, OriginalFunction) in PatchingHelper.PatchedFunctions)
    {
        var thunk = (nuint*)Thunk;

        uint protect;
        if (VirtualProtect(thunk, (nuint)sizeof(nuint), PAGE.PAGE_READWRITE, &protect))
        {
            *thunk = OriginalFunction;
            VirtualProtect(thunk, (nuint)sizeof(nuint), protect, &protect);
        }
    }

    var edge = GetModuleHandleA((sbyte*)Unsafe.AsPointer(in MemoryMarshal.GetReference("edgehtml.dll"u8)));
    if (edge.Value is not null)
    {
        var dllmain = (delegate* unmanaged[Stdcall]<HINSTANCE, uint, void*, BOOL>)PatchingHelper.GetModuleEntryPoint(edge);
        if (dllmain is not null)
        {
            LOG_LAST_ERROR_IF(!dllmain(edge, DLL_PROCESS_DETACH, null));
        }
    }
}
And this fixes the problem! That was all, thank you for reading!
Last modified on February 20, 2026