diff --git a/Common/CodeBlock.h b/Common/CodeBlock.h index 04642f16b4..853c15c149 100644 --- a/Common/CodeBlock.h +++ b/Common/CodeBlock.h @@ -99,8 +99,8 @@ public: // If not WX Exclusive, no need to call ProtectMemoryPages because we never change the protection from RWX. PoisonMemory(offset); ResetCodePtr(offset); - if (PlatformIsWXExclusive() && offset != 0) { - // Need to re-protect the part we didn't clear. + if (PlatformIsWXExclusive()) { + // Need to re-protect the part we didn't clear (with MAP_JIT, this thread's view of it all). ProtectMemoryPages(region, offset, MEM_PROT_READ | MEM_PROT_EXEC); } } diff --git a/Common/MemoryUtil.cpp b/Common/MemoryUtil.cpp index 7817c37db3..335a280b67 100644 --- a/Common/MemoryUtil.cpp +++ b/Common/MemoryUtil.cpp @@ -41,6 +41,14 @@ #include #endif +#if PPSSPP_PLATFORM(MAC) && PPSSPP_ARCH(ARM64) +#include +#include +#include +#include +#define USE_MAP_JIT +#endif + #ifndef _WIN32 #include #endif @@ -183,7 +191,43 @@ void *AllocateExecutableMemory(size_t size) { #else // Non-Windows platforms +#ifdef USE_MAP_JIT +// With MAP_JIT, a thread switches all of its view of the JIT memory between writable and executable +// with pthread_jit_write_protect_np(), far cheaper than an mprotect() for every block compiled and +// linked. A hardened runtime without the JIT entitlement refuses the mapping: then mprotect() it is. +static std::mutex g_mapJitLock; +static int g_mapJitState; // 0 untried, 1 in use, -1 refused. +static std::vector> g_mapJitRanges; + +static bool IsMapJitMemory(const void *ptr) { + std::lock_guard guard(g_mapJitLock); + for (const auto &range : g_mapJitRanges) { + if ((uintptr_t)ptr >= range.first && (uintptr_t)ptr < range.second) + return true; + } + return false; +} +#endif + void *AllocateExecutableMemory(size_t size) { +#ifdef USE_MAP_JIT + { + std::lock_guard guard(g_mapJitLock); + if (g_mapJitState >= 0) { + void *ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0); + if (ptr != MAP_FAILED) { + g_mapJitState = 1; + g_mapJitRanges.emplace_back((uintptr_t)ptr, (uintptr_t)ptr + size); + return ptr; + } + if (g_mapJitState == 0) { + WARN_LOG(Log::MemMap, "MAP_JIT refused (errno=%d), using mprotect", errno); + g_mapJitState = -1; + } + } + } +#endif + static char *map_hint = nullptr; #if PPSSPP_ARCH(AMD64) @@ -339,6 +383,19 @@ void FreeMemoryPages(void *ptr, size_t size) { } void FreeExecutableMemory(void *ptr, size_t size) { +#ifdef USE_MAP_JIT + if (IsMapJitMemory(ptr)) { + std::lock_guard guard(g_mapJitLock); + for (size_t i = 0; i < g_mapJitRanges.size(); i++) { + if (g_mapJitRanges[i].first == (uintptr_t)ptr) { + g_mapJitRanges.erase(g_mapJitRanges.begin() + i); + break; + } + } + // Back to executable, which the protection change before freeing undid. + pthread_jit_write_protect_np(1); + } +#endif FreeMemoryPages(ptr, size); } @@ -374,6 +431,16 @@ bool ProtectMemoryPages(const void* ptr, size_t size, uint32_t memProtFlags) { _assert_msg_(false, "Bad memory protect flags %d: W^X is in effect, can't both write and exec", memProtFlags); } } +#ifdef USE_MAP_JIT + if (IsMapJitMemory(ptr)) { + _assert_msg_((memProtFlags & (MEM_PROT_WRITE | MEM_PROT_EXEC)) != 0, "MAP_JIT memory is writable or executable"); + pthread_jit_write_protect_np((memProtFlags & MEM_PROT_WRITE) == 0); + return true; + } +#endif + if (size == 0) + return true; + // Note - VirtualProtect will affect the full pages containing the requested range. // mprotect does not seem to, at least not on Android unless I made a mistake somewhere, so we manually round. #ifdef _WIN32