Fix macOS hang when closing the emulator after running a game (#366)

* Fix macOS hang when closing the emulator after running a game

On exit, the static g_time_zone_binary_romfs in nxemu-os still held a
loader-owned interface. Its destructor ran from exit() after the loader
module was destroyed, faulting on a dangling pointer. ResetTimeZoneBinary()
(called from ~TimeManager) now releases it while the loader is alive.

The fault turned into a permanent hang because dynarmic's macOS
MachHandler left its task-wide EXC_BAD_ACCESS port installed and its
detached pump thread running after the CPU module was unloaded. Any later
fault was sent to a port nobody served. The handler now saves and restores
the previous exception ports, destroys its receive right and joins the
pump thread on destruction.

* Restore signal handlers when the CPU module is unloaded on Linux/Android

The dynarmic POSIX handler (x86_64) and the NCE handlers (arm64) installed
SIGSEGV and related handlers from the CPU module and never removed them.
After the module is unloaded, a later fault would jump into unmapped code.
Save the previous handlers and restore them on teardown.
This commit is contained in:
Bruno 2026-10-07 13:05:57 +02:00 committed by GitHub
parent 0f6da343f4
commit e30c654ef6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 77 additions and 9 deletions

View File

@ -6,6 +6,8 @@
#include <mach/mach.h>
#include <mach/message.h>
#include <algorithm>
#include <atomic>
#include <cstring>
#include <functional>
#include <memory>
@ -79,7 +81,15 @@ private:
std::mutex code_block_infos_mutex;
std::thread thread;
mach_port_t server_port;
mach_port_t server_port = MACH_PORT_NULL;
std::atomic<bool> shutting_down{false};
// Exception ports that were installed before ours, restored on shutdown.
exception_mask_t old_masks[EXC_TYPES_COUNT];
mach_port_t old_ports[EXC_TYPES_COUNT];
exception_behavior_t old_behaviors[EXC_TYPES_COUNT];
thread_state_flavor_t old_flavors[EXC_TYPES_COUNT];
mach_msg_type_number_t old_count = 0;
void MessagePump();
};
@ -89,20 +99,41 @@ MachHandler::MachHandler() {
KCHECK(mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port));
KCHECK(mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND));
old_count = EXC_TYPES_COUNT;
KCHECK(task_get_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, old_masks, &old_count, old_ports, old_behaviors, old_flavors));
KCHECK(task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, server_port, EXCEPTION_STATE | MACH_EXCEPTION_CODES, THREAD_STATE));
// The below doesn't actually work, and I'm not sure why; since this doesn't work we'll have a spurious error message upon shutdown.
mach_port_t prev;
KCHECK(mach_port_request_notification(mach_task_self(), server_port, MACH_NOTIFY_PORT_DESTROYED, 0, server_port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &prev));
#undef KCHECK
thread = std::thread(&MachHandler::MessagePump, this);
thread.detach();
}
MachHandler::~MachHandler() {
// The handler lives in the CPU module. When that module is unloaded, this
// destructor runs and the module's code is unmapped right after. The pump
// thread must not outlive it, and the task must not keep routing
// EXC_BAD_ACCESS to a port nobody serves: any later fault (anywhere in the
// process) would then block forever instead of crashing.
shutting_down = true;
for (mach_msg_type_number_t i = 0; i < old_count; i++) {
task_set_exception_ports(mach_task_self(), old_masks[i], old_ports[i], old_behaviors[i], old_flavors[i]);
}
if (old_count == 0) {
task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, MACH_PORT_NULL, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
}
// Destroying the receive right wakes the pump thread out of mach_msg.
mach_port_mod_refs(mach_task_self(), server_port, MACH_PORT_RIGHT_RECEIVE, -1);
if (thread.joinable()) {
thread.join();
}
mach_port_deallocate(mach_task_self(), server_port);
for (mach_msg_type_number_t i = 0; i < old_count; i++) {
if (MACH_PORT_VALID(old_ports[i])) {
mach_port_deallocate(mach_task_self(), old_ports[i]);
}
}
}
void MachHandler::MessagePump() {
@ -112,6 +143,9 @@ void MachHandler::MessagePump() {
while (true) {
mr = mach_msg(&request.head, MACH_RCV_MSG | MACH_RCV_LARGE, 0, sizeof(request), server_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (shutting_down) {
return;
}
if (mr != MACH_MSG_SUCCESS) {
fmt::print(stderr, "dynarmic: macOS MachHandler: Failed to receive mach message. error: {:#08x} ({})\n", mr, mach_error_string(mr));
return;

View File

@ -118,6 +118,14 @@ SigHandler::SigHandler() {
}
SigHandler::~SigHandler() {
// The handler lives in the CPU module: restore the previous handlers so a
// later fault does not jump into the module after it has been unloaded.
if (supports_fast_mem) {
sigaction(SIGSEGV, &old_sa_segv, nullptr);
#ifdef __APPLE__
sigaction(SIGBUS, &old_sa_bus, nullptr);
#endif
}
std::free(signal_stack_memory);
}

View File

@ -24,6 +24,28 @@ namespace
struct sigaction g_orig_bus_action;
struct sigaction g_orig_segv_action;
struct sigaction g_orig_return_to_run_code_action;
struct sigaction g_orig_break_from_run_code_action;
struct sigaction g_orig_alignment_fault_action;
// The handlers below live in the CPU module. Restore the previous ones when the
// module is unloaded, so a later signal does not jump into unmapped code.
struct SignalHandlerRestorer
{
bool installed = false;
~SignalHandlerRestorer()
{
if (!installed)
{
return;
}
Common::SigAction(ReturnToRunCodeByExceptionLevelChangeSignal, &g_orig_return_to_run_code_action, nullptr);
Common::SigAction(BreakFromRunCodeSignal, &g_orig_break_from_run_code_action, nullptr);
Common::SigAction(GuestAlignmentFaultSignal, &g_orig_alignment_fault_action, nullptr);
Common::SigAction(GuestAccessFaultSignal, &g_orig_segv_action, nullptr);
}
} g_signal_handler_restorer;
// Verify assembly offsets.
static_assert(offsetof(NativeExecutionParameters, native_context) == TpidrEl0NativeContext);
@ -271,25 +293,26 @@ void ArmNce::Initialize()
return_to_run_code_action.sa_sigaction = reinterpret_cast<HandlerType>(&ArmNce::ReturnToRunCodeByExceptionLevelChangeSignalHandler);
return_to_run_code_action.sa_mask = signal_mask;
Common::SigAction(ReturnToRunCodeByExceptionLevelChangeSignal, &return_to_run_code_action,
nullptr);
&g_orig_return_to_run_code_action);
struct sigaction break_from_run_code_action{};
break_from_run_code_action.sa_flags = SA_SIGINFO | SA_ONSTACK;
break_from_run_code_action.sa_sigaction = reinterpret_cast<HandlerType>(&ArmNce::BreakFromRunCodeSignalHandler);
break_from_run_code_action.sa_mask = signal_mask;
Common::SigAction(BreakFromRunCodeSignal, &break_from_run_code_action, nullptr);
Common::SigAction(BreakFromRunCodeSignal, &break_from_run_code_action, &g_orig_break_from_run_code_action);
struct sigaction alignment_fault_action{};
alignment_fault_action.sa_flags = SA_SIGINFO | SA_ONSTACK;
alignment_fault_action.sa_sigaction = reinterpret_cast<HandlerType>(&ArmNce::GuestAlignmentFaultSignalHandler);
alignment_fault_action.sa_mask = signal_mask;
Common::SigAction(GuestAlignmentFaultSignal, &alignment_fault_action, nullptr);
Common::SigAction(GuestAlignmentFaultSignal, &alignment_fault_action, &g_orig_alignment_fault_action);
struct sigaction access_fault_action{};
access_fault_action.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART;
access_fault_action.sa_sigaction = reinterpret_cast<HandlerType>(&ArmNce::GuestAccessFaultSignalHandler);
access_fault_action.sa_mask = signal_mask;
Common::SigAction(GuestAccessFaultSignal, &access_fault_action, &g_orig_segv_action);
g_signal_handler_restorer.installed = true;
});
}

View File

@ -35,6 +35,9 @@ Result TimeZoneReadBinary(Core::System& system, uint32_t & out_read_size, std::s
} // namespace
void ResetTimeZoneBinary() {
// The romfs is owned by the loader module. Release it here, while that module
// is still alive, rather than from this static's destructor at process exit.
g_time_zone_binary_romfs = nullptr;
g_time_zone_binary_mount_result = ResultUnknown;
g_time_zone_scratch_space.clear();
g_time_zone_scratch_space.resize(0x2800, 0);