Source/PLCrashMachExceptionServer.m (403 lines of code) (raw):

/* * Author: Landon Fuller <landonf@bikemonkey.org> * * Copyright (c) 2012-2013 Plausible Labs Cooperative, Inc. * All rights reserved. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ #include "PLCrashFeatureConfig.h" #import "PLCrashMachExceptionPort.h" #if PLCRASH_FEATURE_MACH_EXCEPTIONS /* * WARNING: * * I've held off from implementing Mach exception handling due to the fact that the APIs required for a complete * implementation are not public on iOS. However, a commercial crash reporter is now shipping with support for Mach * exceptions, which implies that either they've received special dispensation to use private APIs / private structures, * they've found another way to do it, or they're just using undocumented functionality and hoping for the best. * * After filing a request with Apple DTS to clarify the issue, they provided the following guidance: * Our engineers have reviewed your request and have determined that this would be best handled as a bug report, * which you have already filed. _There is no documented way of accomplishing this, nor is there a workaround * possible._ * * Emphasis mine. As such, I don't believe it is be possible to support the use of Mach exceptions on iOS * without the use of undocumented functionality. * * Unfortunately, sigaltstack() is broken in later iOS releases, necessitating an alternative fix. Even if it wasn't * broken, it only ever supported handling stack overflow on the main thread, and mach exceptions would be a preferrable * solution. * * As such, this file provides a proof-of-concept implementation of Mach exception handling, intended to * provide support for Mac OS X using public API, and to ferret out what cannot be implemented on iOS * without the use of private API on iOS. Some developers have requested that Mach exceptions be provided as * option on iOS, which we may provide in the future. * * The following issues exist in the iOS implementation: * - The msgh_id values required for an exception reply message are not available from the available * headers and must be hard-coded. This prevents one from safely replying to exception messages, which * means that it is impossible to (correctly) inform the server that an exception has *not* been * handled. * * Impact: * This can lead to the process locking up and not dispatching to the host exception handler (eg, Apple's * crash reporter), depending on the behavior of the kernel exception code. * * - The mach_* structure/type variants required by MACH_EXCEPTION_CODES are not publicly defined (on Mac OS X, * these are provided by mach_exc.defs). This prevents one from forwarding exception messages to an existing * handler that was registered with a MACH_EXCEPTION_CODES behavior. * * Impact: * This can break forwarding to any task exception handler that registers itself with MACH_EXCEPTION_CODES. * This is the case with LLDB; it will register a task exception handler with MACH_EXCEPTION_CODES set. Failure * to correctly forward these exceptions will result in the debugger breaking in interesting ways; for example, * changes to the set of dyld-loaded images are detected by setting a breakpoint on the dyld image registration * funtions, and this functionality will break if the exception is not correctly forwarded. * * Since Mach exception handling is important for a fully functional crash reporter, I've also filed a radar * to request that the API be made public: * Radar: rdar://12939497 RFE: Provide mach_exc.defs for iOS */ #import "PLCrashMachExceptionServer.h" #import "PLCrashReporterNSError.h" #import "PLCrashHostInfo.h" #import "PLCrashAsync.h" #import <pthread.h> #import <stdatomic.h> #import <mach/mach.h> #import <mach/exc.h> /* The msgh_id to use for thread termination messages. This value most not conflict with the MACH_NOTIFY_NO_SENDERS msgh_id, which * is the only other value currently sent on the server notify port */ #define PLCRASH_TERMINATE_MSGH_ID 0xDEADBEEF #if PLCRASH_TERMINATE_MSGH_ID == MACH_NOTIFY_NO_SENDERS #error The allocated message identifiers conflict. #endif #if PL_MACH64_EXC_API # import "mach_exc.h" typedef __Request__mach_exception_raise_t PLRequest_exception_raise_t; typedef __Reply__mach_exception_raise_t PLReply_exception_raise_t; #else typedef __Request__exception_raise_t PLRequest_exception_raise_t; typedef __Reply__exception_raise_t PLReply_exception_raise_t; #endif #ifdef PL_MACH64_EXC_CODES # define PLCRASH_DEFAULT_BEHAVIOR (EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES) #else # define PLCRASH_DEFAULT_BEHAVIOR EXCEPTION_DEFAULT #endif /** * @internal * Map an exception type to its corresponding mask value. * * @note This needs to handle all exception types for which the exception server will be registered. */ static exception_mask_t exception_to_mask (exception_type_t exception) { #define EXM(n) case EXC_ ## n: return EXC_MASK_ ## n; switch (exception) { EXM(BAD_ACCESS); EXM(BAD_INSTRUCTION); EXM(ARITHMETIC); EXM(EMULATION); EXM(BREAKPOINT); EXM(SOFTWARE); EXM(SYSCALL); EXM(MACH_SYSCALL); EXM(RPC_ALERT); EXM(CRASH); #ifdef EXC_GUARD EXM(GUARD); #endif } #undef EXM /* This is very loosely gauranteed in exception_types.h; it's possible, though unlikely, that * a future exception type could diverge from the standard mask flag assignment. */ PLCF_DEBUG("Unhandled exception type %d; exception_to_mask() should be updated", exception); #ifdef PLCF_DEBUG_BUILD abort(); #else return (1 << exception); #endif } /** * @internal * * Exception handler context. */ struct plcrash_exception_server_context { /** The server's mach thread. */ thread_t server_thread; /** Registered exception port. */ mach_port_t server_port; /** Notification port */ mach_port_t notify_port; /** Listen port set */ mach_port_t port_set; /** User callback. */ PLCrashMachExceptionHandlerCallback callback; /** User callback context. */ void *callback_context; /** Lock used to signal waiting initialization thread. */ pthread_mutex_t lock; /** Condition used to signal waiting initialization thread. */ pthread_cond_t server_cond; /** * Intended to be set by a controlling termination thread. Informs the mach exception * thread that it should de-register itself and then signal completion. * * This value must be updated atomically and with a memory barrier, as it will be accessed * without locking. */ atomic_bool server_should_stop; /** Intended to be observed by the waiting initialization thread. Informs * the waiting thread that shutdown has completed . */ bool server_stop_done; }; /*** * @internal * * Mach Exception Server. * * Implements monitoring of Mach exceptions on tasks and threads. * * @TODO We need to be able to determine if an exception can be/will/was handled by a signal handler. Failure * to detect such a case will result in spurious reports written for otherwise handled signals. See also: * https://bugzilla.xamarin.com/show_bug.cgi?id=4120 * * @par Double Faults * * It may be valuable to be able to detect that your crash reporter itself crashed, * and if possible, provide debugging information that can be reported. * * How this is handled depends on whether you are running in-process, or out-of-process. * * @par Out-of-process * * In the case that the reporter is running out-of-process, it is recommended that you use a * crash reporter *on your crash reporter* to report the crash. * * It is less likely that a bug triggered by analyzing the target process will <em>also</em> * be triggered when analyzing the crash reporter itself. * * @par In-process * * When running in-process, it is far more likely that re-running the crash reporter * will trigger the same crash again. Thus, it is recommended that an implementor handle double * faults in a "safe mode" less likely to trigger an additional crash, and gauranteed to record * (at a minimum) that the crash report itself crashed, even if no additional crash data can be * recorded. * * This may be done by targeting the Mach exception server's thread with a thread-specific * crash handler. All callbacks will be issued on this thread, and it may be reliably targeted * to observe any crashes that occur within those callbacks. * * An example implementation might do the following: * - Before performing any other operations, create a cookie file on-disk that can be checked on * startup to determine whether the crash reporter itself crashed. This at the very least will * let API clients know that a problem exists (eg, a failure occured while generating the report). * - Re-run the crash report writer, disabling any risky code paths that are not strictly necessary, e.g.: * - Disable local symbolication if it has been enabled by the user. This will avoid * a great deal if binary parsing. * - Disable reporting on any threads other than the crashed thread. This will avoid * any bugs that may have occured in the stack unwinding code for existing threads. */ @implementation PLCrashMachExceptionServer /** * Initialize a new Mach exception server. * * @param callback Callback called upon receipt of an exception. The callback will execute * on the exception server's thread, distinctly from the crashed thread. * @param context Context to be passed to the callback. May be NULL. * @param outError A pointer to an NSError object variable. If an error occurs initializing the exception server, * this pointer will contain an error object in the NSMachErrorDomain or NSPOSIXErrorDomain indicating why the * exception handler could not be registered. If no error occurs, this parameter will be left unmodified. * You may specify NULL for this parameter, and no error information will be provided. */ - (id) initWithCallBack: (PLCrashMachExceptionHandlerCallback) callback context: (void *) context error: (NSError **) outError { pthread_attr_t attr; pthread_t thr; kern_return_t kr; if ((self = [super init]) == nil) return nil; /* Initialize the bare context. */ _serverContext = (struct plcrash_exception_server_context *) calloc(1, sizeof(*_serverContext)); _serverContext->server_port = MACH_PORT_NULL; _serverContext->notify_port = MACH_PORT_NULL; _serverContext->port_set = MACH_PORT_NULL; _serverContext->server_thread = MACH_PORT_NULL; _serverContext->callback = callback; _serverContext->callback_context = context; if (pthread_mutex_init(&_serverContext->lock, NULL) != 0) { plcrash_populate_posix_error(outError, errno, @"Mutex initialization failed"); free(_serverContext); _serverContext = NULL; return nil; } if (pthread_cond_init(&_serverContext->server_cond, NULL) != 0) { plcrash_populate_posix_error(outError, errno, @"Condition initialization failed"); pthread_mutex_destroy(&_serverContext->lock); free(_serverContext); _serverContext = NULL; return nil; } /* * Initalize our server's port */ kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &_serverContext->server_port); if (kr != KERN_SUCCESS) { plcrash_populate_mach_error(outError, kr, @"Failed to allocate exception server's port"); return nil; } /* * Initialize our notification port */ kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &_serverContext->notify_port); if (kr != KERN_SUCCESS) { plcrash_populate_mach_error(outError, kr, @"Failed to allocate exception server's port"); return nil; } kr = mach_port_insert_right(mach_task_self(), _serverContext->notify_port, _serverContext->notify_port, MACH_MSG_TYPE_MAKE_SEND); if (kr != KERN_SUCCESS) { plcrash_populate_mach_error(outError, kr, @"Failed to add send right to exception server's port"); return nil; } mach_port_t prev_notify_port; kr = mach_port_request_notification(mach_task_self(), _serverContext->server_port, MACH_NOTIFY_NO_SENDERS, 1, _serverContext->notify_port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &prev_notify_port); if (kr != KERN_SUCCESS) { plcrash_populate_mach_error(outError, kr, @"Failed to request MACH_NOTIFY_NO_SENDERS on the exception server's port"); return nil; } /* * Initialize our port set. */ kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_PORT_SET, &_serverContext->port_set); if (kr != KERN_SUCCESS) { plcrash_populate_mach_error(outError, kr, @"Failed to allocate exception server's port set"); return nil; } /* Add the service port to the port set */ kr = mach_port_move_member(mach_task_self(), _serverContext->server_port, _serverContext->port_set); if (kr != KERN_SUCCESS) { plcrash_populate_mach_error(outError, kr, @"Failed to add exception server port to port set"); return nil; } /* Add the notify port to the port set */ kr = mach_port_move_member(mach_task_self(), _serverContext->notify_port, _serverContext->port_set); if (kr != KERN_SUCCESS) { plcrash_populate_mach_error(outError, kr, @"Failed to add exception server notify port to port set"); return nil; } /* Spawn the server thread. */ { if (pthread_attr_init(&attr) != 0) { plcrash_populate_posix_error(outError, errno, @"Failed to initialize pthread_attr"); return nil; } pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // TODO - A custom stack should be specified, using high/low guard pages to help prevent overwriting the stack // by crashing code. // pthread_attr_setstack(&attr, sp, stacksize); if (pthread_create(&thr, &attr, &exception_server_thread, _serverContext) != 0) { plcrash_populate_posix_error(outError, errno, @"Failed to create exception server thread"); pthread_attr_destroy(&attr); return nil; } pthread_attr_destroy(&attr); /* Save the thread reference */ _serverContext->server_thread = pthread_mach_thread_np(thr); } return self; } /** * Return the Mach thread on which the exception server is running. * * @warning The behavior of this method is undefined if the receiver * has not been registered as a mach exception server, or has been deregistered. */ - (thread_t) serverThread { NSAssert(_serverContext != NULL, @"No handler registered!"); thread_t result; pthread_mutex_lock(&_serverContext->lock); { result = _serverContext->server_thread; } pthread_mutex_unlock(&_serverContext->lock); return result; } /** * Create and return a new send right for the receiver's Mach exception server. The callee is responsible * for deallocating the send right via mach_port_deallocate or similar. * * @param outError A pointer to an NSError object variable. If an error occurs, this pointer * will contain an error object indicating why the Mach send right could not be created. If no error * occurs, this parameter will be left unmodified. You may specify nil for this parameter, and no error information * will be provided. * @return Returns a valid mach send right on success; on error, MACH_PORT_NULL will be returned. * * @warning The exception server must be registered with a specific thread state type and behavior; these may be * fetched via the preferred PLCrashMachExceptionServer::exceptionPortWithMask:error:. */ - (mach_port_t) copySendRightForServerAndReturningError: (NSError **) outError { mach_port_t result; kern_return_t kr; pthread_mutex_lock(&_serverContext->lock); { /* Insert a send right; this will either create the right, or bump the reference count. */ kr = mach_port_insert_right(mach_task_self(), _serverContext->server_port, _serverContext->server_port, MACH_MSG_TYPE_MAKE_SEND); if (kr != KERN_SUCCESS) { plcrash_populate_mach_error(outError, kr, @"Failed to insert Mach send right"); pthread_mutex_unlock(&_serverContext->lock); return MACH_PORT_NULL; } result = _serverContext->server_port; }; pthread_mutex_unlock(&_serverContext->lock); return result; } /** * Create and return a new exception port instance for the receiver's Mach exception server. The returned instance * defines the behavior and flavor required by the Mach exception server, as well as providing a valid Mach * send right for the exception server. * * @param outError A pointer to an NSError object variable. If an error occurs, this pointer * will contain an error object in the NSMachErrorDomain indicating why the Mach send right could not be created. If no error * occurs, this parameter will be left unmodified. You may specify nil for this parameter, and no error information * will be provided. * @return Returns a valid Mach port instance on success; on error, nil will be returned. * * @note The newly allocated send right will be owned by the PLCrashMachExceptionPort instance; to ensure that the send * right survives for the lifetime of any exception server registration, the PLCrashMachExceptionPort must either be preserved, * or the underlying mach port's reference count should be incremented, eg, via mach_port_mod_refs() or mach_port_insert_right(). */ - (PLCrashMachExceptionPort *) exceptionPortWithMask: (exception_mask_t) mask error: (NSError **) outError { /* Fetch a send right. Unless misconfigured, this should never fail */ mach_port_t port = [self copySendRightForServerAndReturningError: outError]; if (!MACH_PORT_VALID(port)) return nil; /* Create the port oject */ PLCrashMachExceptionPort *result; result = [[PLCrashMachExceptionPort alloc] initWithServerPort: port mask: mask behavior: PLCRASH_DEFAULT_BEHAVIOR flavor: MACHINE_THREAD_STATE]; /* Drop our send right */ mach_port_deallocate(mach_task_self(), port); return result; } /** * Send a Mach exception reply for the given @a request and return the result. * * @param request The request to which a reply should be sent. * @param retcode The reply return code to supply. */ static mach_msg_return_t exception_server_reply (PLRequest_exception_raise_t *request, kern_return_t retcode) { PLReply_exception_raise_t reply; /* Initialize the reply */ memset(&reply, 0, sizeof(reply)); reply.Head.msgh_bits = MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(request->Head.msgh_bits), 0); reply.Head.msgh_local_port = MACH_PORT_NULL; reply.Head.msgh_remote_port = request->Head.msgh_remote_port; reply.Head.msgh_size = sizeof(reply); reply.NDR = NDR_record; reply.RetCode = retcode; /* * Mach uses reply id offsets of 100. This is rather arbitrary, and in theory could be changed * in a future iOS release (although, it has stayed constant for nearly 24 years, so it seems unlikely * to change now). See the top-level file warning regarding use on iOS. * * On Mac OS X, the reply_id offset may be considered implicitly defined due to mach_exc.defs and * exc.defs being public. */ reply.Head.msgh_id = request->Head.msgh_id + 100; /* Dispatch the reply */ return mach_msg(&reply.Head, MACH_SEND_MSG, reply.Head.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); } /** * Forward a Mach exception to the given exception to the first matching handler in @a state, if any. * * @param task The task in which the exception occured. * @param thread The thread on which the exception occured. The thread will be suspended when the callback is issued, and may be resumed * by the callback using thread_resume(). * @param exception_type Mach exception type. * @param code Mach exception codes. * @param code_count The number of codes provided. * @param port_state The set of exception handlers to which the message should be forwarded. * * @return Returns KERN_SUCCESS if the exception was handled by a registered exception server, or an error * if the exception was not handled, or forwarding failed. * * @par In-Process Operation * * When operating in-process, handling the exception replies internally breaks external debuggers, * as they assume it is safe to leave our thread suspended. This results in the target thread never resuming, * as our thread never wakes up to reply to the message, or to handle future messages. * * The recommended solution is to simply not register a Mach exception handler in the case where a debugger * is already attached. * * @note This function may be called at crash-time. */ kern_return_t PLCrashMachExceptionForward (task_t task, thread_t thread, exception_type_t exception_type, mach_exception_data_t code, mach_msg_type_number_t code_count, plcrash_mach_exception_port_set_t *port_state) { #pragma clang diagnostic push /** * Disable uninitialized variable warnings for `behavior`, `flavor`, `port` and `thread_state_count` which will be triggered * if the build setting for Uninitialized Variables Warning is Yes (Aggressive) * behavior, flavor and port can be seen to be either initialized in the loop (found -> true) or the function will exit (found -> false) * thread_state_count will be initialized for all cases except behavior == EXCEPTION_DEFAULT, in which case the thread_state_count is not used in the `switch (behavior)` block */ #pragma clang diagnostic ignored "-Wconditional-uninitialized" exception_behavior_t behavior; thread_state_flavor_t flavor; mach_port_t port; /* Find a matching handler */ exception_mask_t fwd_mask = exception_to_mask(exception_type); bool found = false; for (mach_msg_type_number_t i = 0; i < port_state->count; i++) { if (!MACH_PORT_VALID(port_state->ports[i])) continue; if ((port_state->masks[i] & fwd_mask) == 0) continue; found = true; port = port_state->ports[i]; behavior = port_state->behaviors[i]; flavor = port_state->flavors[i]; break; } /* No handler found */ if (!found) { return KERN_FAILURE; } thread_state_data_t thread_state; mach_msg_type_number_t thread_state_count; kern_return_t kr; /* We prefer 64-bit codes; if the user requests 32-bit codes, we need to map them */ exception_data_type_t code32[code_count]; for (mach_msg_type_number_t i = 0; i < code_count; i++) { code32[i] = (exception_data_type_t)code[i]; } /* Strip the MACH_EXCEPTION_CODES modifier from the behavior flags */ bool mach_exc_codes = false; if (behavior & MACH_EXCEPTION_CODES) { mach_exc_codes = true; behavior &= ~MACH_EXCEPTION_CODES; } /* * Fetch thread state if required. When not required, 'flavor' will be invalid (eg, THREAD_STATE_NONE or similar), and * fetching the thread state will simply fail. */ if (behavior != EXCEPTION_DEFAULT) { thread_state_count = THREAD_STATE_MAX; kr = thread_get_state (thread, flavor, thread_state, &thread_state_count); if (kr != KERN_SUCCESS) { PLCF_DEBUG("Failed to fetch thread state for thread=0x%x, flavor=0x%x, kr=0x%x", thread, flavor, kr); return kr; } } /* Handle the supported behaviors */ switch (behavior) { case EXCEPTION_DEFAULT: if (mach_exc_codes) { #if PL_MACH64_EXC_API return mach_exception_raise(port, thread, task, exception_type, code, code_count); #endif } else { return exception_raise(port, thread, task, exception_type, code32, code_count); } break; case EXCEPTION_STATE: if (mach_exc_codes) { #if PL_MACH64_EXC_API return mach_exception_raise_state(port, exception_type, code, code_count, &flavor, thread_state, thread_state_count, thread_state, &thread_state_count); #endif } else { return exception_raise_state(port, exception_type, code32, code_count, &flavor, thread_state, thread_state_count, thread_state, &thread_state_count); } break; case EXCEPTION_STATE_IDENTITY: if (mach_exc_codes) { #if PL_MACH64_EXC_API return mach_exception_raise_state_identity(port, thread, task, exception_type, code, code_count, &flavor, thread_state, thread_state_count, thread_state, &thread_state_count); #endif } else { return exception_raise_state_identity(port, thread, task, exception_type, code32, code_count, &flavor, thread_state, thread_state_count, thread_state, &thread_state_count); } break; default: /* Handled below */ break; } PLCF_DEBUG("Unsupported exception behavior: 0x%x (MACH_EXCEPTION_CODES=%s)", behavior, mach_exc_codes ? "true" : "false"); return KERN_FAILURE; #pragma clang diagnostic pop } /** * Background exception server. Handles incoming exception messages and dispatches * them to the registered callback. * * This code must be written to be async-safe once a Mach exception message * has been returned, as the state of the process' threads is entirely unknown. */ static void *exception_server_thread (void *arg) { struct plcrash_exception_server_context *exc_context = (struct plcrash_exception_server_context *) arg; PLRequest_exception_raise_t *request = NULL; size_t request_size; kern_return_t kr; mach_msg_return_t mr; /* Initialize the received message with a default size */ request_size = round_page(sizeof(*request)); kr = vm_allocate(mach_task_self(), (vm_address_t *) &request, request_size, VM_FLAGS_ANYWHERE); if (kr != KERN_SUCCESS) { /* Shouldn't happen ... */ fprintf(stderr, "Unexpected error in vm_allocate(): %x\n", kr); return NULL; } /* Wait for an exception message */ while (true) { /* Initialize our request message */ request->Head.msgh_local_port = exc_context->port_set; request->Head.msgh_size = (mach_msg_size_t)request_size; mr = mach_msg(&request->Head, MACH_RCV_MSG | MACH_RCV_LARGE, 0, request->Head.msgh_size, exc_context->port_set, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); /* Handle recoverable errors */ if (mr != MACH_MSG_SUCCESS && mr == MACH_RCV_TOO_LARGE) { /* Determine the new size (before dropping the buffer) */ request_size = round_page(request->Head.msgh_size); /* Drop the old receive buffer */ vm_deallocate(mach_task_self(), (vm_address_t) request, request_size); /* Re-allocate a larger receive buffer */ kr = vm_allocate(mach_task_self(), (vm_address_t *) &request, request_size, VM_FLAGS_ANYWHERE); if (kr != KERN_SUCCESS) { /* Shouldn't happen ... */ fprintf(stderr, "Unexpected error in vm_allocate(): 0x%x\n", kr); return NULL; } continue; /* Handle fatal errors */ } else if (mr != MACH_MSG_SUCCESS) { /* Shouldn't happen ... */ PLCF_DEBUG("Unexpected error in mach_msg(): 0x%x", mr); // TODO - Should we inform observers? continue; /* Success! */ } else { /* Notify port handling */ if (request->Head.msgh_local_port == exc_context->notify_port) { /* Handle no sender notifications */ if (request->Head.msgh_id == MACH_NOTIFY_NO_SENDERS) { /* TODO: This will be used to dispatch 'no sender' delegate messages, which can be used to track whether * all mach exception clients have terminated. This is primarily useful in determining whether the exception * server can shut down when running out-of-process and potentially servicing exception messages from * multiple tasks. */ continue; } /* Detect termination messages. */ if (request->Head.msgh_id == PLCRASH_TERMINATE_MSGH_ID) { /* We intentionally do not acquire a lock here. It is possible that we've been woken * spuriously with the process in an unknown state, in which case we must not call * out to non-async-safe functions */ if (exc_context->server_should_stop) { /* Inform the requesting thread of completion */ pthread_mutex_lock(&exc_context->lock); { exc_context->server_stop_done = true; pthread_cond_signal(&exc_context->server_cond); } pthread_mutex_unlock(&exc_context->lock); /* Ensure a quick death if we access exc_context after termination */ exc_context = NULL; /* Trigger cleanup */ break; } } } /* Sanity check the message size */ if (request->Head.msgh_size < sizeof(*request)) { PLCF_DEBUG("Unexpected message size of %" PRIu64, (uint64_t) request->Head.msgh_size); /* Provide a negative reply */ mr = exception_server_reply(request, KERN_FAILURE); if (mr != MACH_MSG_SUCCESS) PLCF_DEBUG("Unexpected failure replying to Mach exception message: 0x%x", mr); continue; } /* Map 32-bit codes to 64-bit types. */ #if !PL_MACH64_EXC_CODES mach_exception_data_type_t code64[request->codeCnt]; for (mach_msg_type_number_t i = 0; i < request->codeCnt; i++) { code64[i] = (uint32_t) request->code[i]; } #elif PL_MACH64_EXC_API mach_exception_data_type_t *code64 = request->code; #else /* XXX: When the mach_exc* types are unavailable (eg, iOS), we're forced to cast the 32-bit values to * 64-bit values. Our check below verifies that we won't crash here, but this is arguably inappropriate * use of the API. A request for access to the mach_* APIs has been filed as rdar://12939497 */ /* We round up our allocation to a full page, and reallocate if the allocation isn't large enough; * this verifies that the returned request is large enough to contain 64-bit mach exception code data, * even when using the 32-bit types. */ if (request_size - sizeof(*request) < (sizeof(mach_exception_data_type_t) * request->codeCnt)) { PLCF_DEBUG("Request is too small to contain 64-bit mach exception codes (0x%zu", request_size); continue; } mach_exception_data_type_t *code64 = (mach_exception_data_type_t *) request->code; #endif /* Call our handler. */ kern_return_t exc_result; exc_result = exc_context->callback(request->task.name, request->thread.name, request->exception, code64, request->codeCnt, exc_context->callback_context); /* * Reply to the message. */ mr = exception_server_reply(request, exc_result); if (mr != MACH_MSG_SUCCESS) PLCF_DEBUG("Unexpected failure replying to Mach exception message: 0x%x", mr); } } /* Drop the receive buffer */ if (request != NULL) vm_deallocate(mach_task_self(), (vm_address_t) request, request_size); return NULL; } /* We automatically stop the server on dealloc */ - (void) dealloc { mach_msg_return_t mr; if (_serverContext == NULL) { return; } /* Mark the server for termination */ bool expected = false; atomic_compare_exchange_strong(&_serverContext->server_should_stop, &expected, true); /* Wake up the waiting server */ mach_msg_header_t msg; memset(&msg, 0, sizeof(msg)); msg.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND_ONCE); msg.msgh_local_port = MACH_PORT_NULL; msg.msgh_remote_port = _serverContext->notify_port; msg.msgh_size = sizeof(msg); msg.msgh_id = PLCRASH_TERMINATE_MSGH_ID; mr = mach_msg(&msg, MACH_SEND_MSG, msg.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); if (mr != MACH_MSG_SUCCESS) { PLCR_LOG("Unexpected error sending termination message to background thread: %d", mr); return; } /* Wait for completion */ pthread_mutex_lock(&_serverContext->lock); while (!_serverContext->server_stop_done) { pthread_cond_wait(&_serverContext->server_cond, &_serverContext->lock); } pthread_mutex_unlock(&_serverContext->lock); /* Server is now dead, can clean up all resources */ if (_serverContext->server_port != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), _serverContext->server_port); if (_serverContext->notify_port != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), _serverContext->notify_port); if (_serverContext->port_set != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), _serverContext->port_set); pthread_cond_destroy(&_serverContext->server_cond); pthread_mutex_destroy(&_serverContext->lock); /* Once we've been signaled by the background thread, it will no longer access exc_context */ free(_serverContext); } @end #endif /* PLCRASH_FEATURE_MACH_EXCEPTIONS */