From 7255850e9d4e3904157396ba8abf400901d36da9 Mon Sep 17 00:00:00 2001 From: Billy Laws Date: Mon, 9 Sep 2024 17:43:00 +0000 Subject: [PATCH] ARM64EC: Patch the call checker before calling any syscall exports CEF on Windows patches these before FEX is even loaded, so this needs to be setup as early as possible using some direct syscalls which skip any such patches. --- Source/Windows/ARM64EC/Module.S | 42 +++++++++++++++++++------------ Source/Windows/ARM64EC/Module.cpp | 41 ++++++++++++++++++++++-------- 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/Source/Windows/ARM64EC/Module.S b/Source/Windows/ARM64EC/Module.S index eb739b88f..46c18d884 100644 --- a/Source/Windows/ARM64EC/Module.S +++ b/Source/Windows/ARM64EC/Module.S @@ -91,22 +91,32 @@ ret_sp_misaligned: ldr lr, [lr, #:lo12:X64ReturnInstr] br x17 - // Calls NtContinue directly to allow continuing from a full native context, as the NTDLL NtContinue export takes in - // an x64 context with EC and the conversion to that loses the ARM64EC ABI-disallowed registers that FEX uses. -.global "#NtContinueNative" -"#NtContinueNative": - adrp x16, WineSyscallDispatcher - ldr x16, [x16, #:lo12:WineSyscallDispatcher] - cbz x16, direct_syscall -wine_syscall: - mov x9, x30 - adrp x8, WineNtContinueSyscallId - ldr x8, [x8, #:lo12:WineNtContinueSyscallId] - blr x16 - ret -direct_syscall: - svc #0x43 - ret + // Makes a wrapper for calling a system call directly, skipping the usual ntdll thunks +#define HASH # +#define DIRECT_SYSCALL_WRAPPER(Name, WineIdName, WindowsId) \ + .global Name; \ + Name:; \ + adrp x16, WineSyscallDispatcher; \ + ldr x16, [x16, HASH:lo12:WineSyscallDispatcher]; \ + cbz x16, 1f; \ + mov x9, x30; \ + adrp x8, WineIdName; \ + ldr x8, [x8, HASH:lo12:WineIdName]; \ + blr x16; \ + ret; \ + 1:; \ + svc HASH WindowsId; \ + ret + + // Allows for continuing from a full native context, as the NTDLL NtContinue export takes in an x64 context with EC and + // the conversion to that loses the ARM64EC ABI-disallowed registers that FEX uses. +DIRECT_SYSCALL_WRAPPER("#NtContinueNative", WineNtContinueSyscallId, 0x43) + + // Both of these are wrapped as FEX needs them to setup its call checker at startup time and their NTDLL thunks could + // already be patched by then (and because the call checker isn't installed, their patched x86 versions would be invoked + // when called by FEX). +DIRECT_SYSCALL_WRAPPER("#NtAllocateVirtualMemoryNative", WineNtAllocateVirtualMemorySyscallId, 0x18) +DIRECT_SYSCALL_WRAPPER("#NtProtectVirtualMemoryNative", WineNtProtectVirtualMemorySyscallId, 0x50) // A replacement for the standard ARM64EC call checker that ignores any FFS patches and always redirects to a function's // native implementation. As the only library FEX calls into is NTDLL, this is done using a LUT generated at init time. diff --git a/Source/Windows/ARM64EC/Module.cpp b/Source/Windows/ARM64EC/Module.cpp index 40aedcbc4..a117286f9 100644 --- a/Source/Windows/ARM64EC/Module.cpp +++ b/Source/Windows/ARM64EC/Module.cpp @@ -70,8 +70,12 @@ uint32_t NtDllRedirectionLUTSize; void* WineSyscallDispatcher; // TODO: this really shouldn't be hardcoded, once wine gains proper syscall thunks this can be dropped. uint64_t WineNtContinueSyscallId = 0x1a; +uint64_t WineNtAllocateVirtualMemorySyscallId = 0xb; +uint64_t WineNtProtectVirtualMemorySyscallId = 0x76; NTSTATUS NtContinueNative(ARM64_NT_CONTEXT* NativeContext, BOOLEAN Alert); +NTSTATUS NtAllocateVirtualMemoryNative(HANDLE, PVOID*, ULONG_PTR, SIZE_T*, ULONG, ULONG); +NTSTATUS NtProtectVirtualMemoryNative(HANDLE, PVOID*, SIZE_T*, ULONG, ULONG*); [[noreturn]] void JumpSetStack(uintptr_t PC, uintptr_t SP); @@ -144,8 +148,7 @@ bool IsDispatcherAddress(uint64_t Address) { } -void FillNtDllLUTs() { - const HMODULE NtDll = GetModuleHandle("ntdll.dll"); +void FillNtDllLUTs(HMODULE NtDll) { NtDllBase = reinterpret_cast(NtDll); ULONG Size; const auto* LoadConfig = @@ -155,7 +158,10 @@ void FillNtDllLUTs() { const auto* RedirectionTableEnd = RedirectionTableBegin + CHPEMetadata->RedirectionMetadataCount; NtDllRedirectionLUTSize = std::prev(RedirectionTableEnd)->Source + 1; - NtDllRedirectionLUT = new uint32_t[NtDllRedirectionLUTSize]; + + SIZE_T AllocSize = NtDllRedirectionLUTSize * sizeof(uint32_t); + NtAllocateVirtualMemoryNative(NtCurrentProcess(), reinterpret_cast(&NtDllRedirectionLUT), 0, &AllocSize, MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE); for (auto It = RedirectionTableBegin; It != RedirectionTableEnd; It++) { NtDllRedirectionLUT[It->Source] = It->Destination; } @@ -171,9 +177,9 @@ void WriteModuleRVA(HMODULE Module, LONG RVA, T Data) { void* ProtAddress = Address; SIZE_T ProtSize = sizeof(T); ULONG Prot; - NtProtectVirtualMemory(NtCurrentProcess(), &ProtAddress, &ProtSize, PAGE_READWRITE, &Prot); + NtProtectVirtualMemoryNative(NtCurrentProcess(), &ProtAddress, &ProtSize, PAGE_READWRITE, &Prot); *reinterpret_cast(Address) = Data; - NtProtectVirtualMemory(NtCurrentProcess(), &ProtAddress, &ProtSize, Prot, nullptr); + NtProtectVirtualMemoryNative(NtCurrentProcess(), &ProtAddress, &ProtSize, Prot, nullptr); } void PatchCallChecker() { @@ -187,6 +193,23 @@ void PatchCallChecker() { WriteModuleRVA(Module, CHPEMetadata->__os_arm64x_dispatch_icall, &CheckCall); WriteModuleRVA(Module, CHPEMetadata->__os_arm64x_dispatch_icall_cfg, &CheckCall); } + +// Syscall thunks may have been patched before FEX has loaded, the default call checker installed by ntdll into FEX will +// try to invoke the JIT when calling such patched syscalls but this obviously doesn't work before FEX is initalised. +// This function parses ntdll and sets up a custom call checker to prevent this, as such it must avoid using any syscall +// thunks itself. +void InitSyscalls() { + // The ntdll exports called by GetModuleHandle/GetProcAddress aren't known to be patched before JIT init by any current + // software so are safe to call, but if that changes the loader structures in the PEB could be parsed manually. + const auto NtDll = GetModuleHandle("ntdll.dll"); + const auto WineSyscallDispatcherPtr = reinterpret_cast(GetProcAddress(NtDll, "__wine_syscall_dispatcher")); + if (WineSyscallDispatcherPtr) { + WineSyscallDispatcher = *WineSyscallDispatcherPtr; + } + + FillNtDllLUTs(NtDll); + PatchCallChecker(); +} } // namespace namespace Exception { @@ -442,6 +465,8 @@ extern "C" void SyncThreadContext(CONTEXT* Context) { } NTSTATUS ProcessInit() { + InitSyscalls(); + FEX::Windows::InitCRTProcess(); FEX::Config::LoadConfig(nullptr, FEX::Windows::GetExecutableFilePath()); FEXCore::Config::ReloadMetaLayer(); @@ -473,14 +498,8 @@ NTSTATUS ProcessInit() { X64ReturnInstr = ::VirtualAlloc(nullptr, FEXCore::Utils::FEX_PAGE_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE); *reinterpret_cast(X64ReturnInstr) = 0xc3; - FillNtDllLUTs(); - PatchCallChecker(); const uintptr_t KiUserExceptionDispatcherFFS = reinterpret_cast(GetProcAddress(NtDll, "KiUserExceptionDispatcher")); Exception::KiUserExceptionDispatcher = NtDllRedirectionLUT[KiUserExceptionDispatcherFFS - NtDllBase] + NtDllBase; - const auto WineSyscallDispatcherPtr = reinterpret_cast(GetProcAddress(NtDll, "__wine_syscall_dispatcher")); - if (WineSyscallDispatcherPtr) { - WineSyscallDispatcher = *WineSyscallDispatcherPtr; - } return STATUS_SUCCESS; }