Skip to content

Instantly share code, notes, and snippets.

@DerekSelander
Last active October 4, 2023 03:01
Show Gist options
  • Save DerekSelander/4c098ba82bd11b537c4ebc2d0bfe44ee to your computer and use it in GitHub Desktop.
Save DerekSelander/4c098ba82bd11b537c4ebc2d0bfe44ee to your computer and use it in GitHub Desktop.
A quick Darwin helper tool to diagnose why your program keeps crashing
//
// 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