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:
parent
0f6da343f4
commit
e30c654ef6
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue