plcrashreporter/Source/PLCrashMachExceptionServer.m

866 строки
36 KiB
Objective-C

/*
* 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.
*/
#import "PLCrashCompatConstants.h"
#import "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 {
/** Backing server context. This structure will not be allocated until the background
* exception server thread is spawned; once the server thread has been successfully started,
* it is that server thread's responsibility to deallocate this structure. */
struct plcrash_exception_server_context *_serverContext;
}
/**
* 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 */