From 88ab029c742417e1f8ecd425c3f9f600df54da05 Mon Sep 17 00:00:00 2001 From: theofficialgman <28281419+theofficialgman@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:11:32 -0400 Subject: [PATCH] add linux option for guest memory usage --- runtime/include/guest_flat_memory.h | 10 +- runtime/include/system_bridge.h | 15 +- runtime/src/guest_flat_memory.cpp | 212 ++++++++++++++++++++++++---- runtime/src/main.cpp | 107 +++++++++++++- runtime/src/system_bridge.cpp | 12 +- 5 files changed, 315 insertions(+), 41 deletions(-) diff --git a/runtime/include/guest_flat_memory.h b/runtime/include/guest_flat_memory.h index caa71c2..e33573a 100644 --- a/runtime/include/guest_flat_memory.h +++ b/runtime/include/guest_flat_memory.h @@ -73,8 +73,12 @@ FaultCounters Counters(); void LogFaultSummary() noexcept; // Returns true when the access violation was a guest-space fault this module -// resolved; the caller must then resume execution. `exceptionPointers` is a -// Windows EXCEPTION_POINTERS*. -bool HandleAccessViolation(void* exceptionPointers) noexcept; +// resolved; the caller must then resume execution. `faultAddress` is the raw +// host pointer the access violation trapped on (Windows: ExceptionInformation[1]; +// POSIX: siginfo_t::si_addr) and `isWrite` is whether it was a write access +// (Windows: ExceptionInformation[0] != 0; POSIX: derived from the ucontext). +// The platform-specific handler that calls this is expected to have already +// done that extraction - this function only ever works with the parsed pair. +bool HandleAccessViolation(void* faultAddress, bool isWrite) noexcept; } // namespace GuestFlat diff --git a/runtime/include/system_bridge.h b/runtime/include/system_bridge.h index ec76421..1f57f0d 100644 --- a/runtime/include/system_bridge.h +++ b/runtime/include/system_bridge.h @@ -11,10 +11,23 @@ #include "memory.h" +// Windows' SehLogger longjmps out of a vectored exception handler, where plain setjmp/longjmp is +// the norm. A POSIX signal handler jumping back to here must use the sig-prefixed pair instead: +// only sigsetjmp/siglongjmp save and restore the process signal mask, which is what keeps SIGSEGV +// from staying blocked (and a second fault during the same ctor loop from escalating instead of +// trapping) after the first recovered fault. +#if defined(_WIN32) +using MkwJmpBuf = jmp_buf; +#define MKW_SETJMP(buf) setjmp(buf) +#else +using MkwJmpBuf = sigjmp_buf; +#define MKW_SETJMP(buf) sigsetjmp(buf, 1) +#endif + // Global flag to suppress SEH reporting (caught by system_bridge) extern bool g_suppressSehReporting; // Jump buffer for SEH recovery -extern thread_local jmp_buf* g_sehJumpTarget; +extern thread_local MkwJmpBuf* g_sehJumpTarget; // SEH details for the most recent trapped exception (used during ctor execution). extern thread_local uint32_t g_sehLastExceptionCode; extern thread_local uintptr_t g_sehLastExceptionAddress; diff --git a/runtime/src/guest_flat_memory.cpp b/runtime/src/guest_flat_memory.cpp index 2075f42..ae9f3f7 100644 --- a/runtime/src/guest_flat_memory.cpp +++ b/runtime/src/guest_flat_memory.cpp @@ -19,6 +19,7 @@ #include "runtime_log.h" #include "system_bridge.h" +#if defined(_WIN32) #ifndef NOMINMAX #define NOMINMAX #endif @@ -26,28 +27,57 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#else +#include +#include +#include +#include +#include +#endif namespace GuestFlat { namespace { +#if defined(_WIN32) // Placeholder / view constants. Declared here so the build does not depend on // the exact Windows SDK version that first shipped them. constexpr DWORD kMemReplacePlaceholder = 0x00004000; constexpr DWORD kMemReservePlaceholder = 0x00040000; constexpr DWORD kMemPreservePlaceholder = 0x00000002; +#endif constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB constexpr size_t kHostPageSize = 0x1000; +// Named, platform-neutral protection modes so every fault-interception call site below (the +// MMIO window, the executable-write guard, deferred-EFB-read protection, the on-demand +// unmapped-block commit) can stay identical text on both platforms; only ProtectRange() and +// CommitPlaceholder() below branch on VirtualProtect vs. mprotect. +#if defined(_WIN32) +using ProtectionFlags = DWORD; +constexpr ProtectionFlags kProtNone = PAGE_NOACCESS; +constexpr ProtectionFlags kProtRead = PAGE_READONLY; +constexpr ProtectionFlags kProtReadWrite = PAGE_READWRITE; +#else +using ProtectionFlags = int; +constexpr ProtectionFlags kProtNone = PROT_NONE; +constexpr ProtectionFlags kProtRead = PROT_READ; +constexpr ProtectionFlags kProtReadWrite = PROT_READ | PROT_WRITE; +#endif + +#if defined(_WIN32) using VirtualAlloc2Fn = PVOID(WINAPI*)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, void*, ULONG); using MapViewOfFile3Fn = PVOID(WINAPI*)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, void*, ULONG); VirtualAlloc2Fn g_virtualAlloc2 = nullptr; MapViewOfFile3Fn g_mapViewOfFile3 = nullptr; +#endif uint8_t* g_base = nullptr; bool g_initialized = false; std::vector g_activeRegions; +#if defined(_WIN32) PVOID g_vectoredHandle = nullptr; +#endif std::mutex& StateMutex() { static std::mutex mutex; @@ -70,7 +100,11 @@ struct SectionKeyHash { }; struct Section { +#if defined(_WIN32) HANDLE handle = nullptr; +#else + int fd = -1; +#endif uint64_t size = 0; uint8_t* hostView = nullptr; }; @@ -114,6 +148,18 @@ std::vector& DeferredRanges() { return ranges; } +#if !defined(_WIN32) +// Windows disambiguates a racing "unmapped touch" fault via VirtualQuery (did some other thread +// already commit this 64 KiB block, and is it actually accessible enough to satisfy this access). +// mprotect has no query counterpart, so this tracks the same fact ourselves: one bit per 64 KiB +// block, set the first time this module ever commits it, checked-and-set under StateMutex() so +// two threads racing on the same never-yet-committed block still report/commit exactly once. +std::vector& UnmappedCommittedBlocks() { + static std::vector blocks(1u << 16, 0); // 2^32 / 64 KiB + return blocks; +} +#endif + std::atomic g_countMmio{0}; std::atomic g_countEfb{0}; std::atomic g_countXGuard{0}; @@ -133,10 +179,26 @@ uint64_t RoundUp(uint64_t value, uint64_t alignment) { std::string LastErrorText(const char* what) { std::ostringstream oss; +#if defined(_WIN32) oss << what << " failed (GetLastError=" << GetLastError() << ")"; +#else + oss << what << " failed (" << std::strerror(errno) << ")"; +#endif return oss.str(); } +// Protects [address, address+size) with `protection`, bridging VirtualProtect (Windows) and +// mprotect (POSIX) so every fault-interception call site below can stay platform-neutral. +bool ProtectRange(uint8_t* address, uint64_t size, ProtectionFlags protection) { +#if defined(_WIN32) + DWORD previous = 0; + return VirtualProtect(address, static_cast(size), protection, &previous) != FALSE; +#else + return mprotect(address, static_cast(size), protection) == 0; +#endif +} + +#if defined(_WIN32) void ResolvePlacementApi() { if (g_virtualAlloc2 != nullptr && g_mapViewOfFile3 != nullptr) return; HMODULE kernelBase = GetModuleHandleW(L"kernelbase.dll"); @@ -153,9 +215,11 @@ void ResolvePlacementApi() { "are unavailable on this system)."); } } +#endif void EnsureReservation() { if (g_base != nullptr) return; +#if defined(_WIN32) ResolvePlacementApi(); void* requested = reinterpret_cast(kFixedFlatGuestBase); @@ -178,25 +242,59 @@ void EnsureReservation() { "usual cause."; throw std::runtime_error(oss.str()); } - if (reserved != requested) { throw std::runtime_error( "The flat guest reservation did not land on the fixed base the translated code was " "compiled against."); } +#else + void* requested = reinterpret_cast(kFixedFlatGuestBase); + + // No MAP_FIXED here (and deliberately no MAP_FIXED_NOREPLACE, which needs Linux 4.17+ - + // this must work on kernels as old as 4.9): `requested` is only a hint. The kernel's + // get_unmapped_area honors a page-aligned hint when the whole range is free, so this lands + // on the fixed base in the normal case; if anything already occupies part of the range, the + // kernel silently picks a different address instead of clobbering it, which the check below + // catches - same "something got there first" contract as the Windows path, without needing + // a specific kernel version. + void* reserved = mmap(requested, kGuestSpaceSize + kAllocationGranularity, kProtNone, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + if (reserved == MAP_FAILED) { + std::ostringstream oss; + oss << "Unable to reserve the 4 GiB flat guest address space at 0x" << std::hex + << reinterpret_cast(requested) << std::dec + << " (" << std::strerror(errno) + << "). The translated code addresses guest memory through this fixed base, so it " + "cannot fall back to another one."; + throw std::runtime_error(oss.str()); + } + if (reserved != requested) { + munmap(reserved, kGuestSpaceSize + kAllocationGranularity); + std::ostringstream oss; + oss << "Unable to reserve the 4 GiB flat guest address space at 0x" << std::hex + << reinterpret_cast(requested) << std::dec + << ". Something else in this process already occupies part of the 16 TiB region - " + "an injected library, an overlay or a debugging tool is the usual cause."; + throw std::runtime_error(oss.str()); + } +#endif + g_base = static_cast(reserved); } +#if defined(_WIN32) // Carves `size` bytes out of the enclosing placeholder so a view or a private // commit can replace it. Splitting an exact-size placeholder is a no-op that // reports ERROR_INVALID_PARAMETER; the caller validates the replacement. void SplitPlaceholder(uint8_t* address, uint64_t size) { VirtualFree(address, static_cast(size), MEM_RELEASE | kMemPreservePlaceholder); } +#endif void MapGuestView(const Section& section, uint64_t sectionOffset, uint32_t guestBase, uint64_t mappedSize) { uint8_t* target = g_base + guestBase; +#if defined(_WIN32) SplitPlaceholder(target, mappedSize); void* view = g_mapViewOfFile3(section.handle, GetCurrentProcess(), target, sectionOffset, static_cast(mappedSize), kMemReplacePlaceholder, @@ -208,16 +306,37 @@ void MapGuestView(const Section& section, uint64_t sectionOffset, uint32_t guest << ")"; throw std::runtime_error(oss.str()); } +#else + // MAP_FIXED is safe (and needs no particular kernel version) here specifically because we're + // deliberately overwriting a sub-range of the PROT_NONE reservation this module already owns + // exclusively (see EnsureReservation) - unlike the initial reservation itself, there's no + // "something else might already be there" concern to guard against. + void* view = mmap(target, static_cast(mappedSize), kProtReadWrite, + MAP_SHARED | MAP_FIXED, section.fd, static_cast(sectionOffset)); + if (view == MAP_FAILED) { + std::ostringstream oss; + oss << "Unable to map guest region 0x" << std::hex << guestBase << " (+0x" << mappedSize + << ") into the flat reservation" << std::dec << " (" << std::strerror(errno) << ")"; + throw std::runtime_error(oss.str()); + } +#endif } // Replaces a placeholder with private committed memory. Used for the MMIO // window (read-only zeros) and for on-demand commits of stray guest pages. -bool CommitPlaceholder(uint8_t* address, uint64_t size, DWORD protection) { +bool CommitPlaceholder(uint8_t* address, uint64_t size, ProtectionFlags protection) { +#if defined(_WIN32) SplitPlaceholder(address, size); void* result = g_virtualAlloc2(GetCurrentProcess(), address, static_cast(size), MEM_RESERVE | MEM_COMMIT | kMemReplacePlaceholder, protection, nullptr, 0); return result != nullptr; +#else + // No separate reserve-vs-commit step is needed: the anonymous PROT_NONE reservation this + // range came from is already demand-zero backed, so mprotect() alone both "commits" and + // protects it. + return ProtectRange(address, size, protection); +#endif } // One definition of the two windows lives in memory_access.h; these are the @@ -237,8 +356,7 @@ void ApplyExecutableProtectionLocked() { for (uint64_t page = first; page < last; page += kHostPageSize) { const uint32_t pageIndex = static_cast(page >> 12); if (protectedPages[pageIndex] != 0) continue; - DWORD previous = 0; - if (VirtualProtect(g_base + page, kHostPageSize, PAGE_READONLY, &previous) != FALSE) { + if (ProtectRange(g_base + page, kHostPageSize, kProtRead)) { protectedPages[pageIndex] = 1; } } @@ -284,8 +402,16 @@ void ZeroMappedStorage() { } } +#if defined(_WIN32) LONG CALLBACK FlatGuestVectoredHandler(EXCEPTION_POINTERS* info) { - if (HandleAccessViolation(info)) { + const auto* record = info->ExceptionRecord; + if (record == nullptr || record->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || + record->NumberParameters < 2) { + return EXCEPTION_CONTINUE_SEARCH; + } + void* faultAddress = reinterpret_cast(record->ExceptionInformation[1]); + const bool isWrite = record->ExceptionInformation[0] != 0; + if (HandleAccessViolation(faultAddress, isWrite)) { return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; @@ -298,6 +424,7 @@ void InstallVectoredHandler() { throw std::runtime_error(LastErrorText("AddVectoredExceptionHandler")); } } +#endif void ReportFatalGuestFault(const char* category, uint32_t guestAddress, bool isWrite, const char* detail) { @@ -377,9 +504,7 @@ void Initialize(const std::vector& regions) { for (const auto& range : DeferredRanges()) { const uint64_t first = static_cast(range.start) & ~(kHostPageSize - 1u); const uint64_t last = RoundUp(range.end, kHostPageSize); - DWORD previous = 0; - VirtualProtect(g_base + first, static_cast(last - first), PAGE_READWRITE, - &previous); + ProtectRange(g_base + first, last - first, kProtReadWrite); } DeferredRanges().clear(); ZeroMappedStorage(); @@ -408,6 +533,7 @@ void Initialize(const std::vector& regions) { const uint64_t rounded = RoundUp(size, kAllocationGranularity); Section section; section.size = rounded; +#if defined(_WIN32) section.handle = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, static_cast(rounded >> 32), static_cast(rounded & 0xFFFFFFFFu), nullptr); @@ -419,6 +545,25 @@ void Initialize(const std::vector& regions) { if (section.hostView == nullptr) { throw std::runtime_error(LastErrorText("MapViewOfFile for the host guest-RAM alias")); } +#else + // The section is an anonymous shared-memory object: the SAME physical pages get mapped + // twice below (once here as the always-accessible host view, once per-region as the + // guest view whose protection the fault handler controls), the same "one backing store, + // two VA aliases" trick CreateFileMapping/MapViewOfFile(3) gives Windows. + section.fd = memfd_create("wiicompiled-guest-ram", MFD_CLOEXEC); + if (section.fd < 0) { + throw std::runtime_error(LastErrorText("memfd_create for guest RAM")); + } + if (ftruncate(section.fd, static_cast(rounded)) != 0) { + throw std::runtime_error(LastErrorText("ftruncate for guest RAM")); + } + section.hostView = static_cast( + mmap(nullptr, static_cast(rounded), kProtReadWrite, MAP_SHARED, section.fd, 0)); + if (section.hostView == MAP_FAILED) { + section.hostView = nullptr; + throw std::runtime_error(LastErrorText("mmap for the host guest-RAM alias")); + } +#endif Sections()[key] = section; } @@ -435,12 +580,14 @@ void Initialize(const std::vector& regions) { // MMIO stays inaccessible in both directions so the vectored handler can report missing HLE; the old // PAGE_READONLY read window that returned zero turned missing devices into silent hangs instead. - if (!CommitPlaceholder(g_base + 0xCC000000u, 0x02000000u, PAGE_NOACCESS)) { + if (!CommitPlaceholder(g_base + 0xCC000000u, 0x02000000u, kProtNone)) { throw std::runtime_error(LastErrorText("committing the no-access MMIO window")); } ApplyExecutableProtectionLocked(); +#if defined(_WIN32) InstallVectoredHandler(); +#endif // Freshly created section objects are demand-zero, so no explicit clear is // needed on the first mapping (that would fault in all 152 MiB at startup). @@ -470,9 +617,7 @@ void ProtectDeferredRange(uint32_t address, size_t length) { std::lock_guard lock(StateMutex()); const uint64_t first = static_cast(address) & ~(kHostPageSize - 1u); const uint64_t last = RoundUp(end, kHostPageSize); - DWORD previous = 0; - if (VirtualProtect(g_base + first, static_cast(last - first), PAGE_NOACCESS, - &previous) == FALSE) { + if (!ProtectRange(g_base + first, last - first, kProtNone)) { // An unmapped destination cannot be trapped; the checked path still // clears the readable bias, so nothing silently reads stale bytes. return; @@ -492,8 +637,7 @@ void UnprotectDeferredRange(uint32_t address, size_t length) { ranges.erase(it); const uint64_t first = static_cast(address) & ~(kHostPageSize - 1u); const uint64_t last = RoundUp(end, kHostPageSize); - DWORD previous = 0; - VirtualProtect(g_base + first, static_cast(last - first), PAGE_READWRITE, &previous); + ProtectRange(g_base + first, last - first, kProtReadWrite); } void RegisterExecutableRange(uint32_t start, uint32_t end) { @@ -541,21 +685,14 @@ void LogFaultSummary() noexcept { std::cerr.flush(); } -bool HandleAccessViolation(void* exceptionPointers) noexcept { - if (!g_initialized || exceptionPointers == nullptr) return false; - auto* info = static_cast(exceptionPointers); - const auto* record = info->ExceptionRecord; - if (record == nullptr || record->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || - record->NumberParameters < 2) { - return false; - } +bool HandleAccessViolation(void* faultAddress, bool isWrite) noexcept { + if (!g_initialized || faultAddress == nullptr) return false; - const uintptr_t fault = static_cast(record->ExceptionInformation[1]); + const uintptr_t fault = reinterpret_cast(faultAddress); const uintptr_t base = reinterpret_cast(g_base); if (fault < base || fault - base >= kGuestSpaceSize) return false; const uint32_t guestAddress = static_cast(fault - base); - const bool isWrite = record->ExceptionInformation[0] != 0; // 1) Deferred (EFB) read: materialize the pending copy and drop the trap for the whole 4 KiB page span, // not just the registered range, since protection is page-granular. Leaving a range registered but @@ -581,9 +718,7 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept { ranges.erase(it); spanFirst = static_cast(rangeStart) & ~(kHostPageSize - 1u); spanLast = RoundUp(rangeEnd, kHostPageSize); - DWORD previous = 0; - VirtualProtect(g_base + spanFirst, static_cast(spanLast - spanFirst), - PAGE_READWRITE, &previous); + ProtectRange(g_base + spanFirst, spanLast - spanFirst, kProtReadWrite); } } if (covered) { @@ -618,9 +753,8 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept { // Those arrive in bulk, so the page is opened permanently // rather than trapping every relocation. std::lock_guard lock(StateMutex()); - DWORD previous = 0; - if (VirtualProtect(g_base + (static_cast(pageIndex) << 12), kHostPageSize, - PAGE_READWRITE, &previous) != FALSE) { + if (ProtectRange(g_base + (static_cast(pageIndex) << 12), kHostPageSize, + kProtReadWrite)) { ExecutableProtectedPages()[pageIndex] = 0; } } @@ -665,6 +799,7 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept { bool committed = false; { std::lock_guard lock(StateMutex()); +#if defined(_WIN32) MEMORY_BASIC_INFORMATION mbi{}; if (VirtualQuery(g_base + blockBase, &mbi, sizeof(mbi)) == 0) return false; if (mbi.State == MEM_COMMIT) { @@ -678,9 +813,24 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept { PAGE_EXECUTE)) != 0; return isWrite ? writable : readable; } - if (!CommitPlaceholder(g_base + blockBase, kAllocationGranularity, PAGE_READWRITE)) { +#else + // mprotect has no VirtualQuery counterpart to ask "is this block already committed and + // how", so this module tracks the same fact itself (UnmappedCommittedBlocks, checked and + // set under this same lock): once a block has been committed READ|WRITE by an earlier + // call here (this thread's or a racing one's), every subsequent fault on it is a no-op + // resume - there is no POSIX equivalent of "committed but insufficiently permissioned" + // for a block only this function ever touches. + const uint32_t blockIndex = static_cast(blockBase / kAllocationGranularity); + if (UnmappedCommittedBlocks()[blockIndex] != 0) { + return true; + } +#endif + if (!CommitPlaceholder(g_base + blockBase, kAllocationGranularity, kProtReadWrite)) { return false; } +#if !defined(_WIN32) + UnmappedCommittedBlocks()[blockIndex] = 1; +#endif committed = true; } if (committed) { diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 0378645..edc6b98 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -37,6 +37,8 @@ #include #include #else +#include +#include #include #endif @@ -988,7 +990,12 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) { // flat module registers its own handler first, but registration order is // not guaranteed once another VEH is installed later, so consult it here // too - resolving a fault twice is a no-op. - if (GuestFlat::HandleAccessViolation(info)) { + if (info->ExceptionRecord != nullptr && + info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && + info->ExceptionRecord->NumberParameters >= 2 && + GuestFlat::HandleAccessViolation( + reinterpret_cast(info->ExceptionRecord->ExceptionInformation[1]), + info->ExceptionRecord->ExceptionInformation[0] != 0)) { return EXCEPTION_CONTINUE_EXECUTION; } if (g_suppressSehReporting && g_sehJumpTarget) { @@ -1059,6 +1066,102 @@ void InstallSehLogger() { g_vectoredSehHandle = AddVectoredExceptionHandler(1, SehLogger); } } +#else +// POSIX counterpart to SehLogger above. Unlike Windows' AddVectoredExceptionHandler, which lets +// GuestFlat and this module each install their own handler and defensively re-check each other, +// sigaction only allows one handler per signal - the second registration replaces the first +// instead of chaining. So this is the single SIGSEGV/SIGBUS handler for the whole process, and it +// owns checking GuestFlat's fault-interception logic first, exactly mirroring the order SehLogger +// already uses on Windows. +void ReportUnhandledSignalFault(int sig, void* faultAddress) { + RT_LOG(RT_TAG_RUNTIME) << "Signal " << sig << " (fault address 0x" << std::hex + << reinterpret_cast(faultAddress) << std::dec << ")"; + if (!g_lastEntryLabel.empty()) { + std::cerr << " while executing " << g_lastEntryLabel; + } + std::cerr << std::endl; + if (const CpuContext* cpu = TryGetCpuContext()) { + RT_LOG(RT_TAG_RUNTIME) << "===== DUMPING CPU STATE =====" << std::endl; + SystemBridge::DumpCpuState(cpu); + } + std::cerr.flush(); +} + +void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) { + void* faultAddress = info != nullptr ? info->si_addr : nullptr; + bool isWrite = false; +#if defined(__x86_64__) + // Standard glibc technique for a POSIX fastmem-style handler: bit 1 (0x2) of the hardware + // error code x86 pushes on a page fault records whether it was a write. + if (ucontextVoid != nullptr) { + auto* uc = static_cast(ucontextVoid); + isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0; + } +#endif + + // Guest-space faults are the flat memory interception mechanism (MMIO, deferred EFB reads, + // the executable-write guard, unmapped pages). Resolving one here means resuming the + // faulting instruction, which just returning from the handler does. + if (faultAddress != nullptr && GuestFlat::HandleAccessViolation(faultAddress, isWrite)) { + return; + } + + if (g_suppressSehReporting && g_sehJumpTarget) { + g_sehLastExceptionCode = static_cast(sig); + g_sehLastExceptionAddress = reinterpret_cast(faultAddress); + g_sehLastAccessType = isWrite ? 1u : 0u; + g_sehLastAccessedAddress = reinterpret_cast(faultAddress); + siglongjmp(*g_sehJumpTarget, 1); + } + if (g_suppressSehReporting) { + // Reporting suppressed but nobody armed a recovery jump: restore the default disposition + // and re-raise so the process still terminates, instead of returning into the same fault. + signal(sig, SIG_DFL); + raise(sig); + return; + } + + // Guard against re-entrancy: if we crash while reporting, don't recurse. + static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT; + if (s_inCrashHandler.test_and_set()) { + std::_Exit(EXIT_FAILURE); + } + + ReportUnhandledSignalFault(sig, faultAddress); + std::ostringstream popupDetails; + popupDetails << "A native signal (" << sig << ") occurred"; + if (!g_lastEntryLabel.empty()) { + popupDetails << " while executing " << g_lastEntryLabel; + } + if (faultAddress != nullptr) { + popupDetails << ".\n\nThe game attempted a " << (isWrite ? "write" : "read") + << " at host address 0x" << std::hex + << reinterpret_cast(faultAddress) << std::dec; + } + popupDetails << ".\n\nThe process transcript and crash log contain the full CPU and stack " + "diagnostics."; + ShowRuntimeFatalPopup("a native crash occurred", popupDetails.str()); + DumpHostStackTrace(); + WriteFatalLogImpl(sig == SIGBUS ? "sigbus" : "sigsegv"); + + std::cerr.flush(); + std::cout.flush(); + std::fflush(stdout); + std::fflush(stderr); + std::_Exit(EXIT_FAILURE); +} + +void InstallPosixMemoryFaultHandler() { + struct sigaction action {}; + action.sa_sigaction = PosixMemoryFaultHandler; + action.sa_flags = SA_SIGINFO; + sigemptyset(&action.sa_mask); + sigaction(SIGSEGV, &action, nullptr); + // A touch beyond a memfd-backed mapping's ftruncate()'d size raises SIGBUS rather than + // SIGSEGV on Linux; region sizing should make this unreachable, but routing it to the same + // handler costs nothing and avoids a silent gap if it ever isn't. + sigaction(SIGBUS, &action, nullptr); +} #endif void AbortSignalHandler(int signum) { @@ -1174,6 +1277,8 @@ int RuntimeMain(int argc, char** argv) { ConfigureWindowsFatalDialogBehavior(); InstallSehLogger(); WindowsTimerResolutionGuard timerResolutionGuard; +#else + InstallPosixMemoryFaultHandler(); #endif InitializeProcessTranscript(argc, argv); std::signal(SIGABRT, AbortSignalHandler); diff --git a/runtime/src/system_bridge.cpp b/runtime/src/system_bridge.cpp index 3d0cb45..8b07d12 100644 --- a/runtime/src/system_bridge.cpp +++ b/runtime/src/system_bridge.cpp @@ -9,10 +9,12 @@ #include #include +#if defined(_WIN32) #ifndef NOMINMAX #define NOMINMAX #endif #include +#endif #include "abi_bridge.h" @@ -26,7 +28,7 @@ // Global flag to suppress SEH reporting during static constructor execution bool g_suppressSehReporting = false; -thread_local jmp_buf* g_sehJumpTarget = nullptr; +thread_local MkwJmpBuf* g_sehJumpTarget = nullptr; thread_local uint32_t g_sehLastExceptionCode = 0; thread_local uintptr_t g_sehLastExceptionAddress = 0; thread_local uintptr_t g_sehLastAccessedAddress = 0; @@ -347,9 +349,9 @@ void SystemBridge::Initialize() { if (funcAddr == 0 || funcAddr == 0xFFFFFFFF) continue; if (TranslatedFunctionRegistry::FindByAddressPtr(funcAddr)) { - jmp_buf jumpBuf; + MkwJmpBuf jumpBuf; g_sehJumpTarget = &jumpBuf; - if (setjmp(jumpBuf) == 0) { + if (MKW_SETJMP(jumpBuf) == 0) { cpu.gpr[1] = 0x81700000u; InvokeIndirectCpu(funcAddr, &cpu); dolCount++; @@ -387,9 +389,9 @@ void SystemBridge::Initialize() { if (funcAddr == 0 || funcAddr == 0xFFFFFFFF) continue; if (TranslatedFunctionRegistry::FindByAddressPtr(funcAddr)) { - jmp_buf jumpBuf; + MkwJmpBuf jumpBuf; g_sehJumpTarget = &jumpBuf; - if (setjmp(jumpBuf) == 0) { + if (MKW_SETJMP(jumpBuf) == 0) { cpu.gpr[1] = 0x81700000u; InvokeIndirectCpu(funcAddr, &cpu); count++;