866 строки
36 KiB
Objective-C
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 */
|