Source/PLCrashReporter.m (473 lines of code) (raw):

/* * Author: Landon Fuller <landonf@plausiblelabs.com> * * Copyright (c) 2008-2009 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. */ #if __has_include(<CrashReporter/CrashReporter.h>) #import <CrashReporter/CrashReporter.h> #import <CrashReporter/PLCrashReporter.h> #else #import "CrashReporter.h" #import "PLCrashReporter.h" #endif #import "PLCrashFeatureConfig.h" #import "PLCrashHostInfo.h" #import "PLCrashSignalHandler.h" #import "PLCrashMachExceptionServer.h" #import "PLCrashFeatureConfig.h" #import "PLCrashAsync.h" #import "PLCrashLogWriter.h" #import "PLCrashFrameWalker.h" #import "PLCrashAsyncMachExceptionInfo.h" #import "PLCrashReporterNSError.h" #import <fcntl.h> #import <dlfcn.h> #import <mach-o/dyld.h> #import <stdatomic.h> /** @internal * CrashReporter cache directory name. */ static NSString *PLCRASH_CACHE_DIR = @"com.plausiblelabs.crashreporter.data"; /** @internal * Crash Report file name. */ static NSString *PLCRASH_LIVE_CRASHREPORT = @"live_report.plcrash"; /** @internal * Directory containing crash reports queued for sending. */ static NSString *PLCRASH_QUEUED_DIR = @"queued_reports"; /** @internal * Maximum number of bytes that will be written to the crash report. * Used as a safety measure in case of implementation malfunction. * * We provide for a generous 256k here. Most crash reports * are approximately 7k, however, we've seen 97k reports * generated by a managed runtime that loads a pathologically * large number (~800) of shared libraries. */ #define MAX_REPORT_BYTES (256 * 1024) /** * @internal * Fatal signals to be monitored. */ static int monitored_signals[] = { SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP }; /** @internal * number of signals in the fatal signals list */ static int monitored_signals_count = (sizeof(monitored_signals) / sizeof(monitored_signals[0])); /** * @internal * Signal handler context */ typedef struct signal_handler_ctx { /** PLCrashLogWriter instance */ plcrash_log_writer_t writer; /** Path to the output file */ const char *path; #if PLCRASH_FEATURE_MACH_EXCEPTIONS /* Previously registered Mach exception ports, if any. Will be left uninitialized if PLCrashReporterSignalHandlerTypeMach * is not enabled. */ plcrash_mach_exception_port_set_t port_set; #endif /* PLCRASH_FEATURE_MACH_EXCEPTIONS */ } plcrashreporter_handler_ctx_t; /** * @internal * * Shared dyld image list. */ static plcrash_async_image_list_t shared_image_list; /** * @internal * * Signal handler context (singleton) */ static plcrashreporter_handler_ctx_t signal_handler_context; /** * @internal * * The optional user-supplied callbacks invoked after the crash report has been written. */ static PLCrashReporterCallbacks crashCallbacks = { .version = 0, .context = NULL, .handleSignal = NULL }; /** * Write a fatal crash report. * * @param sigctx Fatal handler context. * @param crashed_thread The crashed thread. * @param thread_state The crashed thread's state. * @param siginfo The signal information. * * @return Returns PLCRASH_ESUCCESS on success, or an appropriate error value if the report could not be written. */ static plcrash_error_t plcrash_write_report (plcrashreporter_handler_ctx_t *sigctx, thread_t crashed_thread, plcrash_async_thread_state_t *thread_state, plcrash_log_signal_info_t *siginfo) { plcrash_async_file_t file; plcrash_error_t err; /* Open the output file */ int fd = open(sigctx->path, O_RDWR|O_CREAT|O_TRUNC, 0644); if (fd < 0) { PLCF_DEBUG("Could not open the crashlog output file: %s", strerror(errno)); return PLCRASH_EINTERNAL; } /* Initialize the output context */ plcrash_async_file_init(&file, fd, MAX_REPORT_BYTES); /* Write the crash log using the already-initialized writer */ err = plcrash_log_writer_write(&sigctx->writer, crashed_thread, &shared_image_list, &file, siginfo, thread_state); /* Close the writer; this may also fail (but shouldn't) */ if (plcrash_log_writer_close(&sigctx->writer) != PLCRASH_ESUCCESS) { PLCF_DEBUG("Failed to close the log writer"); plcrash_async_file_close(&file); return PLCRASH_EINTERNAL; } /* Finished */ if (!plcrash_async_file_flush(&file)) { PLCF_DEBUG("Failed to flush output file"); plcrash_async_file_close(&file); return PLCRASH_EINTERNAL; } if (!plcrash_async_file_close(&file)) { PLCF_DEBUG("Failed to close output file"); return PLCRASH_EINTERNAL; } return err; } /** * @internal * * Signal handler callback. */ static bool signal_handler_callback (int signal, siginfo_t *info, pl_ucontext_t *uap, void *context, PLCrashSignalHandlerCallback *next) { plcrashreporter_handler_ctx_t *sigctx = context; plcrash_async_thread_state_t thread_state; plcrash_log_signal_info_t signal_info; plcrash_log_bsd_signal_info_t bsd_signal_info; /* Remove all signal handlers -- if the crash reporting code fails, the default terminate * action will occur. * * NOTE: SA_RESETHAND breaks SA_SIGINFO on ARM, so we reset the handlers manually. * http://openradar.appspot.com/11839803 * * TODO: When forwarding signals (eg, to Mono's runtime), resetting the signal handlers * could result in incorrect runtime behavior; we should revisit resetting the * signal handlers once we address double-fault handling. */ for (int i = 0; i < monitored_signals_count; i++) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_DFL; sigemptyset(&sa.sa_mask); sigaction(monitored_signals[i], &sa, NULL); } /* Extract the thread state */ plcrash_async_thread_state_mcontext_init(&thread_state, uap->uc_mcontext); /* Set up the BSD signal info */ bsd_signal_info.signo = info->si_signo; bsd_signal_info.code = info->si_code; bsd_signal_info.address = info->si_addr; signal_info.bsd_info = &bsd_signal_info; signal_info.mach_info = NULL; /* Write the report */ if (plcrash_write_report(sigctx, pl_mach_thread_self(), &thread_state, &signal_info) != PLCRASH_ESUCCESS) return false; /* Call any post-crash callback */ if (crashCallbacks.handleSignal != NULL) crashCallbacks.handleSignal(info, uap, crashCallbacks.context); return false; } #if PLCRASH_FEATURE_MACH_EXCEPTIONS /* State and callback used to generate thread state for the calling mach thread. */ struct mach_exception_callback_live_cb_ctx { plcrashreporter_handler_ctx_t *sigctx; thread_t crashed_thread; plcrash_log_signal_info_t *siginfo; }; static plcrash_error_t mach_exception_callback_live_cb (plcrash_async_thread_state_t *state, void *ctx) { struct mach_exception_callback_live_cb_ctx *plcr_ctx = ctx; return plcrash_write_report(plcr_ctx->sigctx, plcr_ctx->crashed_thread, state, plcr_ctx->siginfo); } static kern_return_t mach_exception_callback (task_t task, thread_t thread, exception_type_t exception_type, mach_exception_data_t code, mach_msg_type_number_t code_count, void *context) { plcrashreporter_handler_ctx_t *sigctx = context; plcrash_log_signal_info_t signal_info; plcrash_log_bsd_signal_info_t bsd_signal_info; plcrash_log_mach_signal_info_t mach_signal_info; plcrash_error_t err; /* Let any other registered server attempt to handle the exception */ if (PLCrashMachExceptionForward(task, thread, exception_type, code, code_count, &sigctx->port_set) == KERN_SUCCESS) return KERN_SUCCESS; /* Set up the BSD signal info */ siginfo_t si; if (!plcrash_async_mach_exception_get_siginfo(exception_type, code, code_count, CPU_TYPE_ANY, &si)) { PLCF_DEBUG("Unexpected error mapping Mach exception to a POSIX signal"); return KERN_FAILURE; } bsd_signal_info.signo = si.si_signo; bsd_signal_info.code = si.si_code; bsd_signal_info.address = si.si_addr; signal_info.bsd_info = &bsd_signal_info; /* Set up the Mach signal info */ mach_signal_info.type = exception_type; mach_signal_info.code = code; mach_signal_info.code_count = code_count; signal_info.mach_info = &mach_signal_info; /* Write the report */ struct mach_exception_callback_live_cb_ctx live_ctx = { .sigctx = sigctx, .crashed_thread = thread, .siginfo = &signal_info }; if ((err = plcrash_async_thread_state_current(mach_exception_callback_live_cb, &live_ctx)) != PLCRASH_ESUCCESS) { PLCF_DEBUG("Failed to write live report: %d", err); return KERN_FAILURE; } /* Call any post-crash callback */ if (crashCallbacks.handleSignal != NULL) { /* * The legacy signal-based callback assumes the availability of a ucontext_t; we mock * an empty value here for the purpose of maintaining backwards compatibility. This behavior * is defined in the PLCrashReporterCallbacks API documentation. */ ucontext_t uctx; _STRUCT_MCONTEXT mctx; /* Populate the mctx */ plcrash_async_memset(&mctx, 0, sizeof(mctx)); /* Configure the ucontext */ plcrash_async_memset(&uctx, 0, sizeof(uctx)); uctx.uc_mcsize = sizeof(mctx); uctx.uc_mcontext = &mctx; crashCallbacks.handleSignal(&si, &uctx, crashCallbacks.context); } return KERN_FAILURE; } #endif /* PLCRASH_FEATURE_MACH_EXCEPTIONS */ /** * @internal * dyld image add notification callback. */ static void image_add_callback (const struct mach_header *mh, intptr_t vmaddr_slide) { Dl_info info; /* Look up the image info */ if (dladdr(mh, &info) == 0) { PLCR_LOG("%s: dladdr(%p, ...) failed", __FUNCTION__, mh); return; } /* Register the image */ plcrash_nasync_image_list_append(&shared_image_list, (pl_vm_address_t) mh, info.dli_fname); } /** * @internal * dyld image remove notification callback. */ static void image_remove_callback (const struct mach_header *mh, intptr_t vmaddr_slide) { plcrash_nasync_image_list_remove(&shared_image_list, (uintptr_t) mh); } /** * @internal * * Uncaught exception handler. Sets the plcrash_log_writer_t's uncaught exception * field, and then triggers a SIGTRAP (synchronous exception) to cause a normal * exception dump. */ static void uncaught_exception_handler (NSException *exception) { /** * It is possible that another crash may occur between setting the uncaught * exception field, and triggering the signal handler. */ static atomic_bool exception_is_handled = false; bool expected = false; if (!atomic_compare_exchange_strong(&exception_is_handled, &expected, true)) { return; } /* Set the uncaught exception */ plcrash_log_writer_set_exception(&signal_handler_context.writer, exception); /* Synchronously trigger the crash handler */ abort(); } @interface PLCrashReporter (PrivateMethods) - (id) initWithBundle: (NSBundle *) bundle configuration: (PLCrashReporterConfig *) configuration; - (id) initWithApplicationIdentifier: (NSString *) applicationIdentifier appVersion: (NSString *) applicationVersion appMarketingVersion: (NSString *) applicationMarketingVersion configuration: (PLCrashReporterConfig *) configuration; #if PLCRASH_FEATURE_MACH_EXCEPTIONS - (PLCrashMachExceptionServer *) enableMachExceptionServerWithPreviousPortSet: (__strong PLCrashMachExceptionPortSet **) previousPortSet callback: (PLCrashMachExceptionHandlerCallback) callback context: (void *) context error: (NSError **) outError; #endif - (plcrash_async_symbol_strategy_t) mapToAsyncSymbolicationStrategy: (PLCrashReporterSymbolicationStrategy) strategy; - (BOOL) populateCrashReportDirectoryAndReturnError: (NSError **) outError; - (NSString *) crashReportDirectory; - (NSString *) queuedCrashReportDirectory; @end /** * Crash Reporter. * * A PLCrashReporter instance manages process-wide handling of crashes. */ @implementation PLCrashReporter + (void) initialize { if (![[self class] isEqual: [PLCrashReporter class]]) return; /* Enable dyld image monitoring */ plcrash_nasync_image_list_init(&shared_image_list, mach_task_self()); _dyld_register_func_for_add_image(image_add_callback); _dyld_register_func_for_remove_image(image_remove_callback); } /* (Deprecated) Crash reporter singleton. */ static PLCrashReporter *sharedReporter = nil; /** * Return the default crash reporter instance. The returned instance will be configured * appropriately for release deployment. * * @deprecated As of PLCrashReporter 1.2, the default reporter instance has been deprecated, and API * clients should initialize a crash reporter instance directly. */ + (PLCrashReporter *) sharedReporter { static dispatch_once_t onceLock; dispatch_once(&onceLock, ^{ if (sharedReporter == nil) sharedReporter = [[PLCrashReporter alloc] initWithBundle: [NSBundle mainBundle] configuration: [PLCrashReporterConfig defaultConfiguration]]; }); return sharedReporter; } /** * Initialize a new PLCrashReporter instance with a default configuration appropraite * for release deployment. */ - (instancetype) init { return [self initWithConfiguration: [PLCrashReporterConfig defaultConfiguration]]; } /** * Initialize a new PLCrashReporter instance with the given configuration. * * @param configuration The configuration to be used by this reporter instance. */ - (instancetype) initWithConfiguration: (PLCrashReporterConfig *) configuration { return [self initWithBundle: [NSBundle mainBundle] configuration: configuration]; } /** * Returns YES if the application has previously crashed and * an pending crash report is available. */ - (BOOL) hasPendingCrashReport { /* Check for a live crash report file */ return [[NSFileManager defaultManager] fileExistsAtPath: [self crashReportPath]]; } /** * If an application has a pending crash report, this method returns the crash * report data. * * You may use this to submit the report to your own HTTP server, over e-mail, or even parse and * introspect the report locally using the PLCrashReport API. * * @return Returns nil if the crash report data could not be loaded. */ - (NSData *) loadPendingCrashReportData { return [self loadPendingCrashReportDataAndReturnError: NULL]; } /** * If an application has a pending crash report, this method returns the crash * report data. * * You may use this to submit the report to your own HTTP server, over e-mail, or even parse and * introspect the report locally using the PLCrashReport API. * @param outError A pointer to an NSError object variable. If an error occurs, this pointer * will contain an error object indicating why the pending crash report could not be * loaded. 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 nil if the crash report data could not be loaded. */ - (NSData *) loadPendingCrashReportDataAndReturnError: (NSError **) outError { /* Load the (memory mapped) data */ return [NSData dataWithContentsOfFile: [self crashReportPath] options: NSMappedRead error: outError]; } /** * Purge a pending crash report. * * @return Returns YES on success, or NO on error. */ - (BOOL) purgePendingCrashReport { return [self purgePendingCrashReportAndReturnError: NULL]; } /** * Purge a pending crash report. * * @return Returns YES on success, or NO on error. */ - (BOOL) purgePendingCrashReportAndReturnError: (NSError **) outError { return [[NSFileManager defaultManager] removeItemAtPath: [self crashReportPath] error: outError]; } /** * Enable the crash reporter. Once called, all application crashes will * result in a crash report being written prior to application exit. * * @return Returns YES on success, or NO if the crash reporter could * not be enabled. * * @par Registering Multiple Reporters * * Only one PLCrashReporter instance may be enabled in a process; attempting to enable an additional instance * will return NO, and the reporter will not be enabled. This restriction may be removed in a future release. */ - (BOOL) enableCrashReporter { return [self enableCrashReporterAndReturnError: nil]; } /** * Enable the crash reporter. Once called, all application crashes will * result in a crash report being written prior to application exit. * * This method must only be invoked once. Further invocations will throw * a PLCrashReporterException. * * @param outError A pointer to an NSError object variable. If an error occurs, this pointer * will contain an error in the PLCrashReporterErrorDomain indicating why the Crash Reporter * could not be enabled. 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 YES on success, or NO if the crash reporter could * not be enabled. * * @par Registering Multiple Reporters * * Only one PLCrashReporter instance may be enabled in a process; attempting to enable an additional instance * will return NO and a PLCrashReporterErrorResourceBusy error, and the reporter will not be enabled. * This restriction may be removed in a future release. */ - (BOOL) enableCrashReporterAndReturnError: (NSError **) outError { /* Prevent enabling more than one crash reporter, process wide. We can not support multiple chained reporters * due to the use of NSUncaughtExceptionHandler (it doesn't support chaining or assocation of context with the callbacks), as * well as our legacy approach of deregistering any signal handlers upon the first signal. Once PLCrashUncaughtExceptionHandler is * implemented, and we support double-fault handling without resetting the signal handlers, we can support chaining of multiple * crash reporters. */ { static BOOL enforceOne = NO; pthread_mutex_t enforceOneLock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&enforceOneLock); { if (enforceOne) { pthread_mutex_unlock(&enforceOneLock); plcrash_populate_error(outError, PLCrashReporterErrorResourceBusy, @"A PLCrashReporter instance has already been enabled", nil); return NO; } enforceOne = YES; } pthread_mutex_unlock(&enforceOneLock); } /* Check for programmer error */ if (_enabled) [NSException raise: PLCrashReporterException format: @"The crash reporter has already been enabled"]; /* Create the directory tree */ if (![self populateCrashReportDirectoryAndReturnError: outError]) return NO; /* Set up the signal handler context */ signal_handler_context.path = strdup([[self crashReportPath] UTF8String]); // NOTE: would leak if this were not a singleton struct assert(_applicationIdentifier != nil); assert(_applicationVersion != nil); plcrash_log_writer_init(&signal_handler_context.writer, _applicationIdentifier, _applicationVersion, _applicationMarketingVersion, [self mapToAsyncSymbolicationStrategy: _config.symbolicationStrategy], false); /* Set custom data, if already set before enabling */ if (self.customData != nil) { plcrash_log_writer_set_custom_data(&signal_handler_context.writer, self.customData); } /* Enable the signal handler */ switch (_config.signalHandlerType) { case PLCrashReporterSignalHandlerTypeBSD: for (size_t i = 0; i < monitored_signals_count; i++) { if (![[PLCrashSignalHandler sharedHandler] registerHandlerForSignal: monitored_signals[i] callback: &signal_handler_callback context: &signal_handler_context error: outError]) return NO; } break; #if PLCRASH_FEATURE_MACH_EXCEPTIONS case PLCrashReporterSignalHandlerTypeMach: { /* We still need to use signal handlers to catch SIGABRT in-process. The kernel sends an EXC_CRASH mach exception * to denote SIGABRT termination. In that case, catching the Mach exception in-process leads to process deadlock * in an uninterruptable wait. Thus, we fall back on BSD signal handlers for SIGABRT, and do not register for * EXC_CRASH. */ if (![[PLCrashSignalHandler sharedHandler] registerHandlerForSignal: SIGABRT callback: &signal_handler_callback context: &signal_handler_context error: outError]) return NO; /* Enable the server. */ _machServer = [self enableMachExceptionServerWithPreviousPortSet: &_previousMachPorts callback: &mach_exception_callback context: &signal_handler_context error: outError]; if (_machServer == nil) return NO; /* * MEMORY WARNING: To ensure that our instance survives for the lifetime of the callback registration, * we keep a reference on self. This is necessary to ensure that the Mach exception server instance and previous port set * survive for the lifetime of the callback. Since there's currently no support for *deregistering* a crash reporter, * this simply results in the reporter living forever. */ CFBridgingRetain(self); /* * Save the previous ports. There's a race condition here, in that an exception that is delivered before (or during) * setting the previous port values will see a fully and/or partially configured port set. This could be an issue * when interoperating with managed runtimes, where NULL dereferences may trigger exception handling * in a common runtime case. * * TODO: Investigate use of (async-safe) locking to close the window in which an exception would not be safely forwarded. * This issue also exists (and is noted with a TODO) in PLCrashSignalHandler. */ signal_handler_context.port_set = [_previousMachPorts asyncSafeRepresentation]; break; } #endif /* PLCRASH_FEATURE_MACH_EXCEPTIONS */ } /* Set the uncaught exception handler */ if(_config.shouldRegisterUncaughtExceptionHandler) { NSSetUncaughtExceptionHandler(&uncaught_exception_handler); } /* Success */ _enabled = YES; return YES; } /** * Generate a live crash report for a given @a thread, without triggering an actual crash condition. * This may be used to log current process state without actually crashing. The crash report data will be * returned on success. * * @param thread The thread which will be marked as the failing thread in the generated report. * * @return Returns nil if the crash report data could not be generated. * * @sa PLCrashReporter::generateLiveReportWithMachThread:error: */ - (NSData *) generateLiveReportWithThread: (thread_t) thread { return [self generateLiveReportWithThread: thread error: NULL]; } /* State and callback used by -generateLiveReportWithThread */ struct plcr_live_report_context { plcrash_log_writer_t *writer; plcrash_async_file_t *file; plcrash_log_signal_info_t *info; }; static plcrash_error_t plcr_live_report_callback (plcrash_async_thread_state_t *state, void *ctx) { struct plcr_live_report_context *plcr_ctx = ctx; return plcrash_log_writer_write(plcr_ctx->writer, pl_mach_thread_self(), &shared_image_list, plcr_ctx->file, plcr_ctx->info, state); } /** * Generate a live crash report for a given @a thread, without triggering an actual crash condition. * This may be used to log current process state without actually crashing. The crash report data will be * returned on success. * * @param thread The thread which will be marked as the failing thread in the generated report. * @param outError A pointer to an NSError object variable. If an error occurs, this pointer * will contain an error object indicating why the crash report could not be generated or loaded. 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 nil if the crash report data could not be loaded. * * @todo Implement in-memory, rather than requiring writing of the report to disk. */ - (NSData *) generateLiveReportWithThread: (thread_t) thread error: (NSError **) outError { plcrash_log_writer_t writer; plcrash_async_file_t file; plcrash_error_t err; /* Open the output file */ NSString *templateStr = [NSTemporaryDirectory() stringByAppendingPathComponent: @"live_crash_report.XXXXXX"]; char *path = strdup([templateStr fileSystemRepresentation]); int fd = mkstemp(path); if (fd < 0) { plcrash_populate_posix_error(outError, errno, NSLocalizedString(@"Failed to create temporary path", @"Error opening temporary output path")); free(path); return nil; } /* Initialize the output context */ plcrash_log_writer_init(&writer, _applicationIdentifier, _applicationVersion, _applicationMarketingVersion, [self mapToAsyncSymbolicationStrategy: _config.symbolicationStrategy], true); plcrash_async_file_init(&file, fd, MAX_REPORT_BYTES); /* Set custom data, if already set before enabling */ if (self.customData != nil) { plcrash_log_writer_set_custom_data(&writer, self.customData); } /* Mock up a SIGTRAP-based signal info */ plcrash_log_bsd_signal_info_t bsd_signal_info; plcrash_log_signal_info_t signal_info; bsd_signal_info.signo = SIGTRAP; bsd_signal_info.code = TRAP_TRACE; bsd_signal_info.address = __builtin_return_address(0); signal_info.bsd_info = &bsd_signal_info; signal_info.mach_info = NULL; /* Write the crash log using the already-initialized writer */ if (thread == pl_mach_thread_self()) { struct plcr_live_report_context ctx = { .writer = &writer, .file = &file, .info = &signal_info }; err = plcrash_async_thread_state_current(plcr_live_report_callback, &ctx); } else { err = plcrash_log_writer_write(&writer, thread, &shared_image_list, &file, &signal_info, NULL); } plcrash_log_writer_close(&writer); /* Flush the data */ plcrash_async_file_flush(&file); plcrash_async_file_close(&file); /* Check for write failure */ NSData *data; if (err != PLCRASH_ESUCCESS) { PLCR_LOG("Write failed with error %s", plcrash_async_strerror(err)); plcrash_populate_error(outError, PLCrashReporterErrorUnknown, @"Failed to write the crash report to disk", nil); data = nil; goto cleanup; } data = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String: path] options:NSDataReadingMappedAlways | NSDataReadingUncached error:outError]; if (data == nil) { /* This should only happen if our data is deleted out from under us */ plcrash_populate_error(outError, PLCrashReporterErrorUnknown, NSLocalizedString(@"Unable to open live crash report for reading", nil), nil); goto cleanup; } cleanup: /* Finished -- clean up. */ plcrash_log_writer_free(&writer); if (unlink(path) != 0) { /* This shouldn't fail, but if it does, there's no use in returning nil */ PLCR_LOG("Failure occured deleting live crash report: %s", strerror(errno)); } free(path); return data; } /** * Generate a live crash report, without triggering an actual crash condition. This may be used to log * current process state without actually crashing. The crash report data will be returned on * success. * * @return Returns nil if the crash report data could not be loaded. */ - (NSData *) generateLiveReport { return [self generateLiveReportAndReturnError: NULL]; } /** * Generate a live crash report for the current thread, without triggering an actual crash condition. * This may be used to log current process state without actually crashing. The crash report data will be * returned on success. * * @param outError A pointer to an NSError object variable. If an error occurs, this pointer * will contain an error object indicating why the pending crash report could not be * generated or loaded. 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 nil if the crash report data could not be loaded. */ - (NSData *) generateLiveReportAndReturnError: (NSError **) outError { return [self generateLiveReportWithThread: pl_mach_thread_self() error: outError]; } /** * Set the callbacks that will be executed by the receiver after a crash has occured and been recorded by PLCrashReporter. * * @param callbacks A pointer to an initialized PLCrashReporterCallbacks structure. * * @note This method must be called prior to PLCrashReporter::enableCrashReporter or * PLCrashReporter::enableCrashReporterAndReturnError: * * @sa The @ref async_safety documentation. */ - (void) setCrashCallbacks: (PLCrashReporterCallbacks *) callbacks { /* Check for programmer error; this should not be called after the signal handler is enabled as to ensure that * the signal handler can never fire with a partially initialized callback structure. */ if (_enabled) [NSException raise: PLCrashReporterException format: @"The crash reporter has already been enabled"]; assert(callbacks->version == 0); /* Re-initialize our internal callback structure */ crashCallbacks.version = 0; /* Re-configure the saved callbacks */ crashCallbacks.context = callbacks->context; crashCallbacks.handleSignal = callbacks->handleSignal; } /** * Set the custom data that will be saved in the crash report along the rest of information, * It deletes any previous custom data configured. * * @param customData A string with the custom data to save. */ - (void) setCustomData: (NSData *) customData { _customData = customData; plcrash_log_writer_set_custom_data(&signal_handler_context.writer, customData); } - (NSString *) crashReportPath { return [[self crashReportDirectory] stringByAppendingPathComponent: PLCRASH_LIVE_CRASHREPORT]; } @end /** * @internal * * Private Methods */ @implementation PLCrashReporter (PrivateMethods) /** * @internal * * This is the designated initializer, but it is not intended * to be called externally. * * @param applicationIdentifier The application identifier to be included in crash reports. * @param applicationVersion The application version number to be included in crash reports. * @param applicationMarketingVersion The application marketing version number to be included in crash reports. * @param configuration The PLCrashReporter configuration. * * @todo The appId and version values should be fetched from the PLCrashReporterConfig, once the API * has been extended to allow supplying these values. */ - (id) initWithApplicationIdentifier: (NSString *) applicationIdentifier appVersion: (NSString *) applicationVersion appMarketingVersion: (NSString *) applicationMarketingVersion configuration: (PLCrashReporterConfig *) configuration { /* Initialize our superclass */ if ((self = [super init]) == nil) return nil; /* Save the configuration */ _config = configuration; _applicationIdentifier = applicationIdentifier; _applicationVersion = applicationVersion; _applicationMarketingVersion = applicationMarketingVersion; /* No occurances of '/' should ever be in a bundle ID, but just to be safe, we escape them */ NSString *appIdPath = [applicationIdentifier stringByReplacingOccurrencesOfString: @"/" withString: @"_"]; NSString *basePath = _config.basePath; if (basePath == nil) { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); basePath = [paths objectAtIndex: 0]; } _crashReportDirectory = [[basePath stringByAppendingPathComponent: PLCRASH_CACHE_DIR] stringByAppendingPathComponent: appIdPath]; return self; } /** * @internal * * Derive the bundle identifier and version from @a bundle. * * @param bundle The application's main bundle. * @param configuration The PLCrashReporter configuration to use for this instance. */ - (id) initWithBundle: (NSBundle *) bundle configuration: (PLCrashReporterConfig *) configuration { NSString *bundleIdentifier = [bundle bundleIdentifier]; NSString *bundleVersion = [[bundle infoDictionary] objectForKey: (NSString *) kCFBundleVersionKey]; NSString *bundleMarketingVersion = [[bundle infoDictionary] objectForKey: @"CFBundleShortVersionString"]; /* Verify that the identifier is available */ if (bundleIdentifier == nil) { const char *progname = getprogname(); if (progname == NULL) { [NSException raise: PLCrashReporterException format: @"Can not determine process identifier or process name"]; return nil; } PLCR_LOG("Warning -- bundle identifier, using process name %s", progname); bundleIdentifier = [NSString stringWithUTF8String: progname]; } /* Verify that the version is available */ if (bundleVersion == nil) { PLCR_LOG("Warning -- bundle version unavailable"); bundleVersion = @""; } return [self initWithApplicationIdentifier: bundleIdentifier appVersion: bundleVersion appMarketingVersion:bundleMarketingVersion configuration: configuration]; } #if PLCRASH_FEATURE_MACH_EXCEPTIONS /** * Create, register, and return a Mach exception server. * * @param[out] previousPortSet The previously registered Mach exception ports. * @param context The context to be provided to the callback. * @param outError A pointer to an NSError object variable. If an error occurs, this pointer * will contain an error in the PLCrashReporterErrorDomain indicating why the Crash Reporter * could not be enabled. If no error occurs, this parameter will be left unmodified. You may * specify nil for this parameter, and no error information will be provided. */ - (PLCrashMachExceptionServer *) enableMachExceptionServerWithPreviousPortSet: (__strong PLCrashMachExceptionPortSet **) previousPortSet callback: (PLCrashMachExceptionHandlerCallback) callback context: (void *) context error: (NSError **) outError { /* Determine the target exception type mask. Note that unlike some other Mach exception-based * crash reporting implementations, we do not monitor EXC_RESOURCE: * * EXC_RESOURCE wasn't added until iOS 5.1 and Mac OS X 10.8, and is used for * kernel-based thread resource constraints on a per-thread/per-task basis. XNU * supports either pausing threads that exceed the defined constraints (via the private * ledger kernel APIs), or issueing a Mach exception that can be used to monitor the * constraints. * * The EXC_RESOURCE resouce exception is used, for example, to implement the * private posix_spawnattr_setcpumonitor() API, which allows for monitoring CPU utilization * by observing issued EXC_RESOURCE exceptions. This appears to be used by launchd. * * Either way, we're uninterested in EXC_RESOURCE; the xnu ux_exception() handler should not deliver * a signal for the exception and should return KERN_SUCCESS, letting exception_triage() * consider it as handled. */ exception_mask_t exc_mask = EXC_MASK_BAD_ACCESS | /* Memory access fail */ EXC_MASK_BAD_INSTRUCTION | /* Illegal instruction */ EXC_MASK_ARITHMETIC | /* Arithmetic exception (eg, divide by zero) */ EXC_MASK_SOFTWARE | /* Software exception (eg, as triggered by x86's bound instruction) */ EXC_MASK_BREAKPOINT; /* Trace or breakpoint */ /* EXC_GUARD was added in xnu 13.x (iOS 6.0, Mac OS X 10.9) */ #ifdef EXC_MASK_GUARD PLCrashHostInfo *hinfo = [PLCrashHostInfo currentHostInfo]; if (hinfo != nil && hinfo.darwinVersion.major >= 13) exc_mask |= EXC_MASK_GUARD; /* Process accessed a guarded file descriptor. See also: https://devforums.apple.com/message/713907#713907 */ #endif /* Create the server */ NSError *osError; PLCrashMachExceptionServer *server = [[PLCrashMachExceptionServer alloc] initWithCallBack: callback context: context error: &osError]; if (server == nil) { plcrash_populate_error(outError, PLCrashReporterErrorOperatingSystem, @"Failed to instantiate the Mach exception server.", osError); return nil; } /* Allocate the port */ PLCrashMachExceptionPort *port = [server exceptionPortWithMask: exc_mask error: &osError]; if (port == nil) { plcrash_populate_error(outError, PLCrashReporterErrorOperatingSystem, @"Failed to instantiate the Mach exception port.", osError); return nil; } /* Register for the task */ if (![port registerForTask: mach_task_self() previousPortSet: previousPortSet error: &osError]) { plcrash_populate_error(outError, PLCrashReporterErrorOperatingSystem, @"Failed to set the target task's mach exception ports.", osError); return nil; } return server; } #endif /* PLCRASH_FEATURE_MACH_EXCEPTIONS */ /** * Map the configuration defined @a strategy to the backing plcrash_async_symbol_strategy_t representation. * * @param strategy The strategy value to map. */ - (plcrash_async_symbol_strategy_t) mapToAsyncSymbolicationStrategy: (PLCrashReporterSymbolicationStrategy) strategy { plcrash_async_symbol_strategy_t result = PLCRASH_ASYNC_SYMBOL_STRATEGY_NONE; if (strategy == PLCrashReporterSymbolicationStrategyNone) return PLCRASH_ASYNC_SYMBOL_STRATEGY_NONE; if (strategy & PLCrashReporterSymbolicationStrategySymbolTable) result |= PLCRASH_ASYNC_SYMBOL_STRATEGY_SYMBOL_TABLE; if (strategy & PLCrashReporterSymbolicationStrategyObjC) result |= PLCRASH_ASYNC_SYMBOL_STRATEGY_OBJC; return result; } /** * Validate (and create if necessary) the crash reporter directory structure. */ - (BOOL) populateCrashReportDirectoryAndReturnError: (NSError **) outError { NSFileManager *fm = [NSFileManager defaultManager]; /* Set up reasonable directory attributes */ NSDictionary *attributes = [NSDictionary dictionaryWithObject: [NSNumber numberWithUnsignedLong: 0755] forKey: NSFilePosixPermissions]; /* Create the top-level path */ if (![fm fileExistsAtPath: [self crashReportDirectory]] && ![fm createDirectoryAtPath: [self crashReportDirectory] withIntermediateDirectories: YES attributes: attributes error: outError]) { return NO; } /* Create the queued crash report directory */ if (![fm fileExistsAtPath: [self queuedCrashReportDirectory]] && ![fm createDirectoryAtPath: [self queuedCrashReportDirectory] withIntermediateDirectories: YES attributes: attributes error: outError]) { return NO; } return YES; } /** * Return the path to the crash reporter data directory. */ - (NSString *) crashReportDirectory { return _crashReportDirectory; } /** * Return the path to to-be-sent crash reports. */ - (NSString *) queuedCrashReportDirectory { return [[self crashReportDirectory] stringByAppendingPathComponent: PLCRASH_QUEUED_DIR]; } @end