Last active
October 4, 2023 03:01
-
-
Save DerekSelander/4c098ba82bd11b537c4ebc2d0bfe44ee to your computer and use it in GitHub Desktop.
A quick Darwin helper tool to diagnose why your program keeps crashing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// A simple arm64[e] launcher program that catches program crashes and spits out every thread's state and backtrace | |
// | |
// dbgspawn.c | |
// Created by Derek Selander on 9/27/23. | |
// Permissive License: do whatever, so long as you keep this header & note that I am not responsible for any damages | |
// | |
/* To build for iOS on macOS | |
xcrun -sdk iphoneos clang dbgspawn.c -o /tmp/dbgspawn -arch arm64 -arch arm64e | |
echo "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgcGxpc3QgUFVCTElDICItLy9BcHBsZS8vRFREIFBMSVNUIDEuMC8vRU4iICJodHRwczovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+PHBsaXN0IHZlcnNpb249IjEuMCI+PGRpY3Q+PGtleT5jb20uYXBwbGUuc3lzdGVtLXRhc2stcG9ydHM8L2tleT48dHJ1ZS8+PGtleT5wbGF0Zm9ybS1hcHBsaWNhdGlvbjwva2V5Pjx0cnVlLz48a2V5PnJ1bi11bnNpZ25lZC1jb2RlPC9rZXk+PHRydWUvPjxrZXk+dGFza19mb3JfcGlkLWFsbG93PC9rZXk+PHRydWUvPjwvZGljdD48L3BsaXN0Pgo=" | base64 -d > /tmp/entitlements.xml | |
codesign -f -s - /tmp/dbgspawn --entitlements /tmp/entitlements.xml | |
*/ | |
#include <stdio.h> | |
#include <spawn.h> | |
#include <signal.h> | |
#include <stdlib.h> | |
#include <unistd.h> | |
#include <stdbool.h> | |
#include <mach/mach.h> | |
#include <ptrauth.h> | |
#include <libgen.h> | |
#include <stdlib.h> | |
#include <dlfcn.h> | |
#pragma mark - defines - | |
#define handle_sadness(RET_, EXP_) { int V_ = (RET_); if (V_ != (EXP_)){ fprintf(stderr, "[%s:%d] err: %d\n", __FUNCTION__, __LINE__, V_); exit(1);} } | |
#define log_err(STR_, ...) fprintf(stderr, "[%s:%d] " STR_, __FUNCTION__, __LINE__, ##__VA_ARGS__) | |
#define log_out(STR_, ...) fprintf(stdout, STR_, ##__VA_ARGS__) | |
#define do_bind(PTR_, H_) PTR_ = dlsym(H_, #PTR_); if (PTR_ == NULL) { log_err("couldn't bind %s\n", #PTR_); } | |
// suspends execution when catching an exception | |
#define DBG_ENV_VAR "DEBUGME" | |
// posix spawns an iOS app that launches it via the "GUI way" | |
#define APP_LAUNCH_ENV_VAR "APP" | |
#ifndef _POSIX_SPAWN_DISABLE_ASLR | |
#define _POSIX_SPAWN_DISABLE_ASLR 0x0100 | |
#endif | |
#pragma mark - CoreSymbolication - | |
// https://github.com/mountainstorm/CoreSymbolication/blob/master/CoreSymbolication/CoreSymbolication.h | |
struct sCSTypeRef { | |
void* csCppData; // typically retrieved using CSCppSymbol...::data(csData & 0xFFFFFFF8) | |
void* csCppObj; // a pointer to the actual CSCppObject | |
}; | |
struct sCSRange { | |
unsigned long long location; | |
unsigned long long length; | |
}; | |
typedef struct sCSRange CSRange; | |
typedef struct sCSTypeRef CSTypeRef; | |
typedef CSTypeRef CSSymbolicatorRef; | |
typedef CSTypeRef CSSymbolOwnerRef; | |
typedef CSTypeRef CSSymbolRef; | |
static void* no_op(void) { return NULL; } | |
static CSSymbolicatorRef (*CSSymbolicatorCreateWithTask)(task_t task) = (void*)no_op; | |
static CSSymbolRef (*CSSymbolicatorGetSymbolWithAddressAtTime)(CSSymbolicatorRef cs, vm_address_t addr, uint64_t time) = (void*)no_op;; | |
static const char* (*CSSymbolGetMangledName)(CSSymbolRef sym) = (void*)no_op;; | |
static CSRange (*CSSymbolGetRange)(CSSymbolRef sym)= (void*)no_op;; | |
static CSSymbolOwnerRef (*CSSymbolGetSymbolOwner)(CSSymbolRef sym) = (void*)no_op;; | |
static const char* (*CSSymbolOwnerGetPath)(CSSymbolOwnerRef owner) = (void*)no_op;; | |
static bool (*CSIsNull)(CSTypeRef cs) = (void*)no_op;; | |
static void (*CSRelease)(CSTypeRef cs) = (void*)no_op; | |
static __attribute__((constructor)) void find_coresymbolication_functions(void) { | |
void *handle = dlopen("/System/Library/PrivateFrameworks/CoreSymbolication.framework/CoreSymbolication", RTLD_NOW); | |
if (!handle) { | |
log_err("Couldn't find CoreSymbolication\n"); | |
return; | |
} | |
do_bind(CSSymbolicatorCreateWithTask, handle); | |
do_bind(CSSymbolicatorGetSymbolWithAddressAtTime, handle); | |
do_bind(CSSymbolGetMangledName, handle); | |
do_bind(CSSymbolGetRange, handle); | |
do_bind(CSSymbolGetSymbolOwner, handle); | |
do_bind(CSSymbolOwnerGetPath, handle); | |
do_bind(CSIsNull, handle); | |
do_bind(CSRelease, handle); | |
} | |
#pragma mark - code - | |
// lifted & tweaked from lldb... thanks y'all | |
pid_t posix_spawn_for_debug(char *const *argv, char *const *envp, | |
const char *working_dir) { | |
pid_t pid = 0; | |
const char *path = argv[0]; | |
posix_spawnattr_t attr; | |
handle_sadness(posix_spawnattr_init(&attr), 0); | |
short flags = POSIX_SPAWN_START_SUSPENDED | | |
POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK | _POSIX_SPAWN_DISABLE_ASLR; | |
sigset_t no_signals; | |
sigset_t all_signals; | |
sigemptyset(&no_signals); | |
sigfillset(&all_signals); | |
posix_spawnattr_setsigmask(&attr, &no_signals); | |
posix_spawnattr_setsigdefault(&attr, &all_signals); | |
// Set the flags we just made into our posix spawn attributes | |
handle_sadness(posix_spawnattr_setflags(&attr, flags), 0); | |
if (working_dir) { | |
chdir(working_dir); | |
} | |
handle_sadness(posix_spawnp(&pid, path, NULL, &attr, (char *const *)argv, (char *const *)envp), 0); | |
posix_spawnattr_destroy(&attr); | |
return pid; | |
} | |
void dump_thread_info(thread_t thread) { | |
arm_thread_state64_t gpr; | |
mach_msg_type_number_t cnt = ARM_THREAD_STATE64_COUNT; | |
handle_sadness(thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&gpr, | |
&cnt), KERN_SUCCESS); | |
log_out(" x0: 0x%016llx x1: 0x%016llx x2: 0x%016llx x3: 0x%016llx\n", gpr.__x[0], gpr.__x[1], gpr.__x[2], gpr.__x[3] ); | |
log_out(" x4: 0x%016llx x5: 0x%016llx x6: 0x%016llx x7: 0x%016llx\n", gpr.__x[4], gpr.__x[5], gpr.__x[6], gpr.__x[7] ); | |
log_out(" x8: 0x%016llx x9: 0x%016llx x10: 0x%016llx x11: 0x%016llx\n", gpr.__x[8], gpr.__x[9], gpr.__x[10], gpr.__x[11] ); | |
log_out(" x12: 0x%016llx x13: 0x%016llx x14: 0x%016llx x15: 0x%016llx\n", gpr.__x[12], gpr.__x[13], gpr.__x[14], gpr.__x[15] ); | |
log_out(" x16: 0x%016llx x17: 0x%016llx x18: 0x%016llx x19: 0x%016llx\n", gpr.__x[16], gpr.__x[17], gpr.__x[18], gpr.__x[19] ); | |
log_out(" x20: 0x%016llx x21: 0x%016llx x22: 0x%016llx x23: 0x%016llx\n", gpr.__x[20], gpr.__x[21], gpr.__x[22], gpr.__x[23] ); | |
log_out(" x24: 0x%016llx x25: 0x%016llx x26: 0x%016llx x27: 0x%016llx\n", gpr.__x[24], gpr.__x[25], gpr.__x[26], gpr.__x[27] ); | |
#if __has_feature(ptrauth_calls) | |
log_out(" x28: 0x%016llx fp: 0x%016lx lr: 0x%016lx sp: 0x%016lx\n", gpr.__x[28], (uintptr_t)gpr.__opaque_fp, (uintptr_t)gpr.__opaque_lr, (uintptr_t)gpr.__opaque_sp ); | |
#else | |
log_out(" x28: 0x%016llx fp: 0x%016llx lr: 0x%016llx sp: 0x%016llx\n", gpr.__x[28], gpr.__fp, gpr.__lr, gpr.__sp ); | |
#endif | |
log_out("\n"); | |
} | |
uintptr_t get_thread_id(thread_t thread) { | |
kern_return_t kr; | |
struct thread_identifier_info info; | |
mach_msg_type_number_t cnt = THREAD_IDENTIFIER_INFO_COUNT; | |
if ((kr = thread_info(thread, THREAD_IDENTIFIER_INFO, (thread_info_t)&info, &cnt)) != KERN_SUCCESS) { | |
log_err("%s %d\n", mach_error_string(kr), kr); | |
return 0; | |
} | |
return info.thread_id; | |
} | |
off_t dump_stack_frame(CSSymbolicatorRef cs, int frame, uintptr_t addr) { | |
const char *module = "???"; | |
const char *symbol = "<private>"; | |
off_t offset = 0; | |
if (!CSIsNull(cs)) { | |
CSSymbolRef sym = CSSymbolicatorGetSymbolWithAddressAtTime(cs, addr, 0); | |
if (!CSIsNull(sym)) { | |
// name could be stripped | |
const char *s = CSSymbolGetMangledName(sym); | |
if (s && strlen(s)) { | |
symbol = s; | |
} | |
CSSymbolOwnerRef owner = CSSymbolGetSymbolOwner(sym); | |
module = CSSymbolOwnerGetPath(owner); | |
CSRange range = CSSymbolGetRange(sym); | |
offset = addr - range.location; | |
} | |
} | |
log_out("%5d: 0x%016lx %s %s + %lld\n", frame, addr, module, symbol, offset); | |
return offset; | |
} | |
void dump_thread_backtrace(CSSymbolicatorRef cs, task_t task, thread_t thread) { | |
kern_return_t kr; | |
arm_thread_state64_t gpr; | |
uintptr_t thread_id = get_thread_id(thread); | |
log_out("tid: %lu (0x%lx) ", thread_id, thread_id); | |
struct thread_extended_info extended; | |
mach_msg_type_number_t cnt = THREAD_EXTENDED_INFO_COUNT; | |
if (thread_info(thread, THREAD_EXTENDED_INFO, (thread_info_t)&extended, &cnt) == KERN_SUCCESS && strlen(extended.pth_name)) { | |
log_out("%s", extended.pth_name); | |
} | |
log_out("\n"); | |
cnt = ARM_THREAD_STATE64_COUNT; | |
if ((kr = thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&gpr, &cnt)) != KERN_SUCCESS) { | |
log_err("%s %d\n", mach_error_string(kr), kr); | |
return; | |
} | |
#if __has_feature(ptrauth_calls) | |
uintptr_t pc = (uintptr_t)gpr.__opaque_pc; | |
pc = (uintptr_t)ptrauth_strip((void*)pc, ptrauth_key_function_pointer); | |
uintptr_t fp = (uintptr_t)gpr.__opaque_fp; | |
fp = (uintptr_t)ptrauth_strip((void*)fp, ptrauth_key_return_address); | |
uintptr_t lr = (uintptr_t)gpr.__opaque_lr; | |
lr = (uintptr_t)ptrauth_strip((void*)lr, ptrauth_key_function_pointer); | |
#else | |
uintptr_t pc = gpr.__pc; | |
uintptr_t fp = gpr.__fp; | |
uintptr_t lr = gpr.__lr; | |
#endif | |
int frames = 1; | |
off_t offset = dump_stack_frame(cs, frames++, pc); | |
// the stack and fp "usually" get setup by the 2nd opcode (i.e. stp x29, x30, [sp, N]) | |
// so if we know the offset is less than the 2nd opcode, then we should include the lr because | |
//the fp won't have it yet This isn't a fullproof technique to resolving all frames but works most of the time : / | |
if ((offset / sizeof(uint32_t)) <= 3) { // sizeof(uint32_t) == arm64 opcode sz | |
dump_stack_frame(cs, frames++, lr); | |
} | |
struct fp_ptr { | |
struct fp_ptr *next; | |
uintptr_t address; | |
} frame; | |
vm_size_t sz = sizeof(struct fp_ptr); | |
while (fp) { | |
if (vm_read_overwrite(task, fp, sz, (vm_address_t)&frame, &sz)) { | |
break; | |
} | |
if (frame.next == NULL) { | |
break; | |
} | |
dump_stack_frame(cs, frames++, frame.address); | |
fp = (uintptr_t)frame.next; | |
fp = (uintptr_t)ptrauth_strip((void*)(fp), ptrauth_key_frame_pointer); | |
} | |
log_out("\n"); | |
} | |
int main(int argc, const char * argv[], const char* envp[]) { | |
// notify.defs/exc.defs? Who needs them! | |
#pragma pack(push, 4) | |
typedef struct { | |
mach_msg_header_t Head; | |
/* start of the kernel processed data */ | |
mach_msg_body_t msgh_body; | |
mach_msg_port_descriptor_t thread; | |
mach_msg_port_descriptor_t task; | |
/* end of the kernel processed data */ | |
NDR_record_t NDR; | |
exception_type_t exception; | |
mach_msg_type_number_t codeCnt; | |
int64_t code[2]; | |
mach_msg_audit_trailer_t trailer; // we're receiving this so trailer here | |
} exc_req; | |
typedef struct { | |
mach_msg_header_t Head; | |
NDR_record_t NDR; | |
kern_return_t RetCode; | |
} exc_resp; | |
#pragma pack(pop) | |
char cwd[1024]; | |
kern_return_t kr = KERN_SUCCESS; | |
mach_port_t exceptionp = MACH_PORT_NULL; | |
mach_port_options_t opt = { .flags = MPO_INSERT_SEND_RIGHT }; | |
exc_req req = {0}; | |
mach_port_t previous; | |
const char *cwd_ptr = getcwd(cwd, sizeof(cwd)); | |
if (argc == 1) { | |
log_out("Launchs an executable, dupping the thread state if it crashes\n\n[ENV_VARS] %s /path/2/executable [launch_args]", basename((char*)argv[1])); | |
log_out("ENV VARS:\n"); | |
log_out("\t" DBG_ENV_VAR " - suspend execution when exception occurs\n"); | |
log_out("\t" APP_LAUNCH_ENV_VAR " - launches an iOS application via the GUI instead of the tty\n"); | |
exit(1); | |
} | |
pid_t child = posix_spawn_for_debug((char *const*)&argv[1], (char *const *)envp, cwd_ptr); | |
task_t child_task = TASK_NULL; | |
bool task4pid_success = (task_for_pid(mach_task_self(), child, &child_task) == KERN_SUCCESS); | |
if (task4pid_success) { | |
handle_sadness(mach_port_construct(mach_task_self(), &opt, 0, &exceptionp), KERN_SUCCESS); | |
handle_sadness(task_set_exception_ports(child_task, EXC_MASK_ALL, exceptionp, EXCEPTION_DEFAULT|MACH_EXCEPTION_CODES, 0), KERN_SUCCESS); | |
handle_sadness(mach_port_request_notification(mach_task_self(), child_task, MACH_NOTIFY_DEAD_NAME, 0, exceptionp, MACH_MSG_TYPE_MAKE_SEND_ONCE, &previous), KERN_SUCCESS); | |
} else { | |
log_err("task_for_pid failed, executing without exception handling\n"); | |
} | |
log_out("pid %d starting\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n", child); | |
kill(child, SIGCONT); | |
if (task4pid_success) { | |
// listens for an exception message (exc.defs) or a notify dead name (notify.defs) for the child's task on exceptionp | |
if ((kr = mach_msg(&req.Head, MACH_RCV_MSG, 0, sizeof(exc_req), exceptionp, MACH_MSG_TIMEOUT_NONE, 0)) != KERN_SUCCESS) { | |
// likely bad message formatting here... | |
log_err("%s %d", mach_error_string(kr), kr); | |
exit(1); | |
} | |
// if we're here we caught a crash or child has exit()ed | |
if (req.Head.msgh_id == 2405) { // 2401-2405 is mig for Crashy McCrash | |
static const char *exception_strs[] = { | |
"EXC_NONE", | |
"EXC_BAD_ACCESS", | |
"EXC_BAD_INSTRUCTION", | |
"EXC_ARITHMETIC", | |
"EXC_EMULATION", | |
"EXC_SOFTWARE", | |
"EXC_BREAKPOINT", | |
"EXC_SYSCALL", | |
"EXC_MACH_SYSCALL", | |
"EXC_RPC_ALERT", | |
"EXC_CRASH", | |
"EXC_RESOURCE", | |
"EXC_GUARD", | |
"EXC_CORPSE_NOTIFY"}; | |
// print the exception thread first | |
log_out("Caught Exception %3d %s [0x%012llx, 0x%012llx]\n", req.exception, exception_strs[req.exception], req.code[0], req.code[1]); | |
CSSymbolicatorRef cs = CSSymbolicatorCreateWithTask(child_task); | |
if (CSIsNull(cs)) { | |
log_err("Couldn't create CSSymbolicatorRef for pid: %d\n", child); | |
} | |
dump_thread_backtrace(cs, req.task.name, req.thread.name); | |
dump_thread_info(req.thread.name); | |
// then iterate any remaining threads | |
uintptr_t crashing_thread_id = get_thread_id(req.thread.name); | |
thread_act_array_t thread_list; | |
mach_msg_type_number_t listCnt; | |
handle_sadness(task_threads(req.task.name, &thread_list, &listCnt), KERN_SUCCESS); | |
for (int i = 0; i < listCnt; i++) { | |
thread_t tr = thread_list[i]; | |
if (crashing_thread_id != get_thread_id(tr)) { | |
dump_thread_backtrace(cs, req.task.name, tr); | |
dump_thread_info(tr); | |
} | |
} | |
handle_sadness(vm_deallocate(mach_task_self(), (vm_address_t)thread_list, listCnt * sizeof(thread_t)), KERN_SUCCESS); | |
CSRelease(cs); | |
if (getenv(DBG_ENV_VAR)) { | |
log_out("hanging out here, press any key to let crash\n"); | |
getchar(); | |
} | |
// At this point we're done processing the exception task, respond to kernel and let process go down | |
exc_resp resp = { | |
.Head = { | |
.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_MOVE_SEND_ONCE, 0, 0, 0), | |
.msgh_size = sizeof(exc_resp), | |
.msgh_remote_port = req.Head.msgh_remote_port, | |
.msgh_local_port = MACH_PORT_NULL, | |
.msgh_id = req.Head.msgh_id + 100, | |
}, | |
.NDR = NDR_record, | |
.RetCode = KERN_FAILURE, // let the process go down, we're done | |
}; | |
if ((kr = mach_msg(&resp.Head, MACH_SEND_MSG, sizeof(resp), 0, 0, MACH_MSG_TIMEOUT_NONE, 0)) != KERN_SUCCESS) { | |
log_err("%s %d", mach_error_string(kr), kr); | |
} | |
} else if (req.Head.msgh_id == 72) { | |
// child has gone away | |
} else { | |
log_err("unknown message sent to us : /\n"); | |
} | |
} else { // task4pid_success | |
int status; | |
waitpid(child, &status, 0); | |
} | |
log_out("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\npid %d finished\n", child); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment