diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ccb95a325..2b20039f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Reset client report counters during initialization ([#1632](https://github.com/getsentry/sentry-native/pull/1632)) - macOS: cache VM regions for FP validation in the new unwinder. ([#1634](https://github.com/getsentry/sentry-native/pull/1634)) - Linux: remove dependency on `stdio` in the unwinder pointer validation code to reduce exposure to async-signal-unsafe functions. ([#1637](https://github.com/getsentry/sentry-native/pull/1637)) +- macOS: replace sandbox-incompatible IPC primitives (`sem_open`, `shm_open`, `fork`) with sandbox-safe alternatives (`pthread_mutex`, file-backed `mmap`, `posix_spawn`) so the native backend works inside App Sandbox. ([#1644](https://github.com/getsentry/sentry-native/pull/1644)) ## 0.13.6 diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 0798c84492..3b8c6cbc31 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -42,7 +42,9 @@ # include # include # if defined(SENTRY_PLATFORM_MACOS) +# include # include +# include # endif #elif defined(SENTRY_PLATFORM_WINDOWS) # include @@ -3061,8 +3063,8 @@ sentry__crash_daemon_main( pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd) #elif defined(SENTRY_PLATFORM_MACOS) int -sentry__crash_daemon_main( - pid_t app_pid, uint64_t app_tid, int notify_pipe_read, int ready_pipe_write) +sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, int notify_pipe_read, + int ready_pipe_write, int shm_fd) #elif defined(SENTRY_PLATFORM_WINDOWS) int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, @@ -3076,7 +3078,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, app_pid, app_tid, notify_eventfd, ready_eventfd); #elif defined(SENTRY_PLATFORM_MACOS) sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon( - app_pid, app_tid, notify_pipe_read, ready_pipe_write); + app_pid, app_tid, notify_pipe_read, ready_pipe_write, shm_fd); #elif defined(SENTRY_PLATFORM_WINDOWS) sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon( app_pid, app_tid, event_handle, ready_event_handle); @@ -3328,21 +3330,105 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, int notify_eventfd, #elif defined(SENTRY_PLATFORM_MACOS) pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, - int notify_pipe_read, int ready_pipe_write, const char *handler_path) + int notify_pipe_read, int ready_pipe_write, int shm_fd, + const char *handler_path) #elif defined(SENTRY_PLATFORM_WINDOWS) pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, HANDLE ready_event_handle, const char *handler_path) #endif { -#if defined(SENTRY_PLATFORM_UNIX) - // Fork and exec sentry-crash executable - // Using exec (not just fork) avoids inheriting sanitizer state and is - // cleaner +#if defined(SENTRY_PLATFORM_MACOS) + // macOS: Use posix_spawn instead of fork+exec for App Sandbox + // compatibility. posix_spawn is Apple's recommended API and works correctly + // in sandboxed processes, unlike fork() which can have issues with sandbox + // inheritance. + + // Resolve daemon path + char daemon_path[SENTRY_CRASH_MAX_PATH]; + if (handler_path && handler_path[0] != '\0') { + strncpy(daemon_path, handler_path, sizeof(daemon_path) - 1); + daemon_path[sizeof(daemon_path) - 1] = '\0'; + } else { + char exe_path[SENTRY_CRASH_MAX_PATH]; + uint32_t exe_size = sizeof(exe_path); + if (_NSGetExecutablePath(exe_path, &exe_size) != 0) { + SENTRY_WARN("Failed to get executable path for daemon"); + return -1; + } + const char *slash = strrchr(exe_path, '/'); + if (!slash + || (size_t)(slash - exe_path + 1) + strlen("sentry-crash") + >= sizeof(daemon_path)) { + SENTRY_WARN("Daemon path too long"); + return -1; + } + size_t dir_len = (size_t)(slash - exe_path + 1); + memcpy(daemon_path, exe_path, dir_len); + strcpy(daemon_path + dir_len, "sentry-crash"); + } + + // Build argument strings (6 args: pid, tid, notify_fd, ready_fd, shm_fd) + char pid_str[32], tid_str[32], notify_str[32], ready_str[32], shm_str[32]; + snprintf(pid_str, sizeof(pid_str), "%d", (int)app_pid); + snprintf(tid_str, sizeof(tid_str), "%" PRIx64, app_tid); + snprintf(notify_str, sizeof(notify_str), "%d", notify_pipe_read); + snprintf(ready_str, sizeof(ready_str), "%d", ready_pipe_write); + snprintf(shm_str, sizeof(shm_str), "%d", shm_fd); + + char *spawn_argv[] = { "sentry-crash", pid_str, tid_str, notify_str, + ready_str, shm_str, NULL }; + + // Set up posix_spawn attributes + posix_spawnattr_t attr; + posix_spawnattr_init(&attr); + // POSIX_SPAWN_SETSID: create new session (like setsid() after fork) + // POSIX_SPAWN_CLOEXEC_DEFAULT: close all fds except explicitly inherited + short spawn_flags = POSIX_SPAWN_SETSID | POSIX_SPAWN_CLOEXEC_DEFAULT; + posix_spawnattr_setflags(&attr, spawn_flags); + + // Explicitly inherit only the fds the daemon needs + posix_spawn_file_actions_t file_actions; + posix_spawn_file_actions_init(&file_actions); + posix_spawn_file_actions_addinherit_np(&file_actions, notify_pipe_read); + posix_spawn_file_actions_addinherit_np(&file_actions, ready_pipe_write); + posix_spawn_file_actions_addinherit_np(&file_actions, shm_fd); + // Open /dev/null on stdin/stdout/stderr so the daemon starts with valid + // standard fds. Without this, POSIX_SPAWN_CLOEXEC_DEFAULT closes them, + // and the first fopen() in the daemon would get fd 0, which the daemon's + // own close(STDIN_FILENO) would then destroy. + // Skip if an IPC fd occupies that slot (e.g. caller closed stdin before + // sentry_init), to avoid clobbering it with /dev/null. + int std_fds[3] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO }; + int std_modes[3] = { O_RDONLY, O_WRONLY, O_WRONLY }; + for (int i = 0; i < 3; i++) { + if (std_fds[i] != notify_pipe_read && std_fds[i] != ready_pipe_write + && std_fds[i] != shm_fd) { + posix_spawn_file_actions_addopen( + &file_actions, std_fds[i], "/dev/null", std_modes[i], 0); + } + } + + pid_t daemon_pid; + int spawn_result = posix_spawn(&daemon_pid, daemon_path, &file_actions, + &attr, spawn_argv, *_NSGetEnviron()); + + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&attr); + + if (spawn_result != 0) { + SENTRY_WARNF("posix_spawn failed for %s: %s", daemon_path, + strerror(spawn_result)); + return -1; + } + + return daemon_pid; + +#elif defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + // Linux: Use fork+exec pid_t daemon_pid = fork(); if (daemon_pid < 0) { - // Fork failed SENTRY_WARN("Failed to fork daemon process"); return -1; } else if (daemon_pid == 0) { @@ -3350,7 +3436,6 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, setsid(); // Clear FD_CLOEXEC on notify and ready fds so they survive exec -# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) int notify_flags = fcntl(notify_eventfd, F_GETFD); if (notify_flags != -1) { fcntl(notify_eventfd, F_SETFD, notify_flags & ~FD_CLOEXEC); @@ -3359,73 +3444,38 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, if (ready_flags != -1) { fcntl(ready_eventfd, F_SETFD, ready_flags & ~FD_CLOEXEC); } -# elif defined(SENTRY_PLATFORM_MACOS) - int notify_flags = fcntl(notify_pipe_read, F_GETFD); - if (notify_flags != -1) { - fcntl(notify_pipe_read, F_SETFD, notify_flags & ~FD_CLOEXEC); - } - int ready_flags = fcntl(ready_pipe_write, F_GETFD); - if (ready_flags != -1) { - fcntl(ready_pipe_write, F_SETFD, ready_flags & ~FD_CLOEXEC); - } -# endif // Convert arguments to strings for exec char pid_str[32], tid_str[32], notify_str[32], ready_str[32]; snprintf(pid_str, sizeof(pid_str), "%d", (int)app_pid); snprintf(tid_str, sizeof(tid_str), "%" PRIx64, app_tid); -# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) snprintf(notify_str, sizeof(notify_str), "%d", notify_eventfd); snprintf(ready_str, sizeof(ready_str), "%d", ready_eventfd); -# elif defined(SENTRY_PLATFORM_MACOS) - snprintf(notify_str, sizeof(notify_str), "%d", notify_pipe_read); - snprintf(ready_str, sizeof(ready_str), "%d", ready_pipe_write); -# endif char *argv[] = { "sentry-crash", pid_str, tid_str, notify_str, ready_str, NULL }; - // If handler_path was explicitly set via options, use it directly. - // Otherwise, look for sentry-crash next to the current executable - // (matching crashpad's behavior). No fallback chain — fail hard so - // configuration issues are visible. if (handler_path && handler_path[0] != '\0') { execv(handler_path, argv); } else { char exe_path[SENTRY_CRASH_MAX_PATH]; - char daemon_path[SENTRY_CRASH_MAX_PATH]; + char daemon_exec_path[SENTRY_CRASH_MAX_PATH]; -# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) ssize_t exe_len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); if (exe_len > 0) { exe_path[exe_len] = '\0'; const char *slash = strrchr(exe_path, '/'); if (slash) { - size_t dir_len = slash - exe_path + 1; + size_t dir_len = (size_t)(slash - exe_path + 1); if (dir_len + strlen("sentry-crash") - < sizeof(daemon_path)) { - memcpy(daemon_path, exe_path, dir_len); - strcpy(daemon_path + dir_len, "sentry-crash"); - execv(daemon_path, argv); + < sizeof(daemon_exec_path)) { + memcpy(daemon_exec_path, exe_path, dir_len); + strcpy(daemon_exec_path + dir_len, "sentry-crash"); + execv(daemon_exec_path, argv); } } } -# elif defined(SENTRY_PLATFORM_MACOS) - uint32_t exe_size = sizeof(exe_path); - if (_NSGetExecutablePath(exe_path, &exe_size) == 0) { - const char *slash = strrchr(exe_path, '/'); - if (slash) { - size_t dir_len = slash - exe_path + 1; - if (dir_len + strlen("sentry-crash") - < sizeof(daemon_path)) { - memcpy(daemon_path, exe_path, dir_len); - strcpy(daemon_path + dir_len, "sentry-crash"); - execv(daemon_path, argv); - } - } - } -# endif } // exec failed - exit with error @@ -3552,13 +3602,24 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, int main(int argc, char **argv) { - // Expected arguments: + // Expected arguments: + // Linux: + // macOS: +# if defined(SENTRY_PLATFORM_MACOS) + if (argc < 6) { + fprintf(stderr, + "Usage: sentry-crash " + " \n"); + return 1; + } +# else if (argc < 5) { fprintf(stderr, "Usage: sentry-crash " "\n"); return 1; } +# endif // Parse arguments pid_t app_pid = (pid_t)strtoul(argv[1], NULL, 10); @@ -3572,8 +3633,9 @@ main(int argc, char **argv) # elif defined(SENTRY_PLATFORM_MACOS) int notify_pipe_read = atoi(argv[3]); int ready_pipe_write = atoi(argv[4]); + int shm_fd_arg = atoi(argv[5]); return sentry__crash_daemon_main( - app_pid, app_tid, notify_pipe_read, ready_pipe_write); + app_pid, app_tid, notify_pipe_read, ready_pipe_write, shm_fd_arg); # elif defined(SENTRY_PLATFORM_WINDOWS) unsigned long long event_handle_val = strtoull(argv[3], NULL, 10); unsigned long long ready_event_val = strtoull(argv[4], NULL, 10); diff --git a/src/backends/native/sentry_crash_daemon.h b/src/backends/native/sentry_crash_daemon.h index eb84b727ee..69c93e9a26 100644 --- a/src/backends/native/sentry_crash_daemon.h +++ b/src/backends/native/sentry_crash_daemon.h @@ -29,7 +29,8 @@ pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd, const char *handler_path); #elif defined(SENTRY_PLATFORM_MACOS) pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, - int notify_pipe_read, int ready_pipe_write, const char *handler_path); + int notify_pipe_read, int ready_pipe_write, int shm_fd, + const char *handler_path); #elif defined(SENTRY_PLATFORM_WINDOWS) pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, HANDLE ready_event_handle, const char *handler_path); @@ -48,7 +49,7 @@ int sentry__crash_daemon_main( pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd); #elif defined(SENTRY_PLATFORM_MACOS) int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, - int notify_pipe_read, int ready_pipe_write); + int notify_pipe_read, int ready_pipe_write, int shm_fd); #elif defined(SENTRY_PLATFORM_WINDOWS) int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, HANDLE ready_event_handle); diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index 1ccedaa4fe..5418d2a55f 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -691,14 +691,20 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) // Dump daemon log for debugging (uses stdio, safe after page allocator // enabled) - if (ipc && ipc->shm_name[0] != '\0' && ctx - && ctx->database_path[0] != '\0') { - // Extract hex ID from shared memory name (format: "/s-XXXXXXXX") + // Extract the shm identifier for log path construction + // macOS: shm_path = "{tmpdir}/.sentry-shm-{id}", Linux: shm_name = + // "/s-{id}" +# if defined(SENTRY_PLATFORM_MACOS) + const char *shm_id_src = ipc ? ipc->shm_path : ""; +# else + const char *shm_id_src = ipc ? ipc->shm_name : ""; +# endif + if (shm_id_src[0] != '\0' && ctx && ctx->database_path[0] != '\0') { + // Extract hex ID after last '-' in shm name/path const char *shm_id = NULL; - for (const char *p = ipc->shm_name; *p; p++) { + for (const char *p = shm_id_src; *p; p++) { if (*p == '-') { shm_id = p + 1; - break; } } diff --git a/src/backends/native/sentry_crash_ipc.c b/src/backends/native/sentry_crash_ipc.c index be6d4bed97..0b596e36ea 100644 --- a/src/backends/native/sentry_crash_ipc.c +++ b/src/backends/native/sentry_crash_ipc.c @@ -303,50 +303,50 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) # include # include # include +# include # include # include # include sentry_crash_ipc_t * -sentry__crash_ipc_init_app(sem_t *init_sem) +sentry__crash_ipc_init_app(sentry_mutex_t *init_mutex) { sentry_crash_ipc_t *ipc = SENTRY_MAKE(sentry_crash_ipc_t); if (!ipc) { return NULL; } ipc->is_daemon = false; - ipc->init_sem = init_sem; // Use provided semaphore (managed by backend) + ipc->init_mutex = init_mutex; - // Create shared memory with unique name based on PID and thread ID - // macOS has a 31-character limit for POSIX shared memory names (PSEMNAMLEN) - // Format: /s-{8_hex_chars} = 11 chars total (well under 31 limit) - // We mix PID and TID to create a unique 32-bit identifier, allowing - // multiple sentry_init() calls from different threads in the same process. + // Build a file path for shared memory using the system temp directory. + // Unlike shm_open(), regular files in $TMPDIR work inside App Sandbox. + // $TMPDIR is per-app in sandbox (e.g. .../Containers/.../T/). uint64_t tid = (uint64_t)pthread_self(); uint32_t id = (uint32_t)((getpid() ^ (tid & 0xFFFFFFFF)) & 0xFFFFFFFF); - snprintf(ipc->shm_name, sizeof(ipc->shm_name), "/s-%08x", id); + const char *tmpdir = getenv("TMPDIR"); + if (!tmpdir) { + tmpdir = "/tmp"; + } + snprintf(ipc->shm_path, sizeof(ipc->shm_path), "%s/.sentry-shm-%08x", + tmpdir, id); - // Acquire semaphore for exclusive access during initialization - if (ipc->init_sem && sem_wait(ipc->init_sem) < 0) { - SENTRY_WARNF( - "failed to acquire initialization semaphore: %s", strerror(errno)); - sentry_free(ipc); - return NULL; + // Acquire mutex for exclusive access during initialization + if (ipc->init_mutex) { + sentry__mutex_lock(ipc->init_mutex); } - // Try to create or open shared memory + // Use file-backed shared memory (sandbox-safe, no shm_open) bool shm_exists = false; - ipc->shm_fd = shm_open(ipc->shm_name, O_CREAT | O_RDWR | O_EXCL, 0600); + ipc->shm_fd = open(ipc->shm_path, O_CREAT | O_RDWR | O_EXCL, 0600); if (ipc->shm_fd < 0 && errno == EEXIST) { - // Shared memory already exists - reuse it shm_exists = true; - ipc->shm_fd = shm_open(ipc->shm_name, O_RDWR, 0600); + ipc->shm_fd = open(ipc->shm_path, O_RDWR, 0600); } if (ipc->shm_fd < 0) { - SENTRY_WARNF("failed to open shared memory: %s", strerror(errno)); - if (ipc->init_sem) { - sem_post(ipc->init_sem); + SENTRY_WARNF("failed to open shared memory file: %s", strerror(errno)); + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); } sentry_free(ipc); return NULL; @@ -354,38 +354,37 @@ sentry__crash_ipc_init_app(sem_t *init_sem) // Verify and resize shared memory (both new and existing) if (shm_exists) { - // Check if existing shared memory has correct size struct stat st; if (fstat(ipc->shm_fd, &st) < 0) { - SENTRY_WARNF("failed to stat shared memory: %s", strerror(errno)); + SENTRY_WARNF( + "failed to stat shared memory file: %s", strerror(errno)); close(ipc->shm_fd); - if (ipc->init_sem) { - sem_post(ipc->init_sem); + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); } sentry_free(ipc); return NULL; } if (st.st_size != SENTRY_CRASH_SHM_SIZE) { - // Existing shm has wrong size, resize it if (ftruncate(ipc->shm_fd, SENTRY_CRASH_SHM_SIZE) < 0) { - SENTRY_WARNF("failed to resize existing shared memory: %s", - strerror(errno)); + SENTRY_WARNF( + "failed to resize shared memory file: %s", strerror(errno)); close(ipc->shm_fd); - if (ipc->init_sem) { - sem_post(ipc->init_sem); + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); } sentry_free(ipc); return NULL; } } } else { - // New shared memory, set size if (ftruncate(ipc->shm_fd, SENTRY_CRASH_SHM_SIZE) < 0) { - SENTRY_WARNF("failed to resize shared memory: %s", strerror(errno)); + SENTRY_WARNF( + "failed to resize shared memory file: %s", strerror(errno)); close(ipc->shm_fd); - shm_unlink(ipc->shm_name); - if (ipc->init_sem) { - sem_post(ipc->init_sem); + unlink(ipc->shm_path); + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); } sentry_free(ipc); return NULL; @@ -398,25 +397,25 @@ sentry__crash_ipc_init_app(sem_t *init_sem) SENTRY_WARNF("failed to map shared memory: %s", strerror(errno)); close(ipc->shm_fd); if (!shm_exists) { - shm_unlink(ipc->shm_name); + unlink(ipc->shm_path); } - if (ipc->init_sem) { - sem_post(ipc->init_sem); + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); } sentry_free(ipc); return NULL; } - // Create pipe for crash notifications (works across fork) + // Create pipe for crash notifications (works across fork/posix_spawn) if (pipe(ipc->notify_pipe) < 0) { SENTRY_WARNF("failed to create notification pipe: %s", strerror(errno)); munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); close(ipc->shm_fd); if (!shm_exists) { - shm_unlink(ipc->shm_name); + unlink(ipc->shm_path); } - if (ipc->init_sem) { - sem_post(ipc->init_sem); + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); } sentry_free(ipc); return NULL; @@ -425,7 +424,7 @@ sentry__crash_ipc_init_app(sem_t *init_sem) // Make write end non-blocking for signal-safe writes fcntl(ipc->notify_pipe[1], F_SETFL, O_NONBLOCK); - // Create pipe for daemon ready signal (works across fork) + // Create pipe for daemon ready signal if (pipe(ipc->ready_pipe) < 0) { SENTRY_WARNF("failed to create ready pipe: %s", strerror(errno)); close(ipc->notify_pipe[0]); @@ -433,17 +432,15 @@ sentry__crash_ipc_init_app(sem_t *init_sem) munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); close(ipc->shm_fd); if (!shm_exists) { - shm_unlink(ipc->shm_name); + unlink(ipc->shm_path); } - if (ipc->init_sem) { - sem_post(ipc->init_sem); + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); } sentry_free(ipc); return NULL; } - // Zero out shared memory only when first created to ensure clean state - // Don't zero existing memory to avoid corrupting state set by other threads if (!shm_exists) { memset(ipc->shmem, 0, SENTRY_CRASH_SHM_SIZE); ipc->shmem->magic = SENTRY_CRASH_MAGIC; @@ -452,40 +449,31 @@ sentry__crash_ipc_init_app(sem_t *init_sem) sentry__atomic_store(&ipc->shmem->sequence, 0); } - // Release semaphore after initialization - if (ipc->init_sem) { - sem_post(ipc->init_sem); + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); } - SENTRY_DEBUGF("initialized crash IPC (shm=%s, pipe=%d/%d)", ipc->shm_name, + SENTRY_DEBUGF("initialized crash IPC (shm=%s, pipe=%d/%d)", ipc->shm_path, ipc->notify_pipe[0], ipc->notify_pipe[1]); return ipc; } sentry_crash_ipc_t * -sentry__crash_ipc_init_daemon( - pid_t app_pid, uint64_t app_tid, int notify_pipe_read, int ready_pipe_write) +sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, + int notify_pipe_read, int ready_pipe_write, int shm_fd) { + (void)app_pid; + (void)app_tid; + sentry_crash_ipc_t *ipc = SENTRY_MAKE(sentry_crash_ipc_t); if (!ipc) { return NULL; } ipc->is_daemon = true; - // Open existing shared memory created by app (using PID and thread ID) - // Must match the format in sentry__crash_ipc_init_app - uint32_t id = (uint32_t)((app_pid ^ (app_tid & 0xFFFFFFFF)) & 0xFFFFFFFF); - snprintf(ipc->shm_name, sizeof(ipc->shm_name), "/s-%08x", id); - - ipc->shm_fd = shm_open(ipc->shm_name, O_RDWR, 0600); - if (ipc->shm_fd < 0) { - SENTRY_WARNF( - "daemon: failed to open shared memory: %s", strerror(errno)); - sentry_free(ipc); - return NULL; - } - + // Use the inherited shm_fd directly (no shm_open needed, sandbox-safe) + ipc->shm_fd = shm_fd; ipc->shmem = mmap(NULL, SENTRY_CRASH_SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, ipc->shm_fd, 0); if (ipc->shmem == MAP_FAILED) { @@ -504,15 +492,15 @@ sentry__crash_ipc_init_daemon( return NULL; } - // Pipes are inherited from parent after fork - assign the fds + // Pipes and shm_fd are inherited from parent via posix_spawn ipc->notify_pipe[0] = notify_pipe_read; - ipc->notify_pipe[1] = -1; // Daemon doesn't write to notify pipe - ipc->ready_pipe[0] = -1; // Daemon doesn't read from ready pipe + ipc->notify_pipe[1] = -1; + ipc->ready_pipe[0] = -1; ipc->ready_pipe[1] = ready_pipe_write; - SENTRY_DEBUGF( - "daemon: attached to crash IPC (shm=%s, notify_pipe=%d, ready_pipe=%d)", - ipc->shm_name, notify_pipe_read, ready_pipe_write); + SENTRY_DEBUGF("daemon: attached to crash IPC (shm_fd=%d, notify_pipe=%d, " + "ready_pipe=%d)", + shm_fd, notify_pipe_read, ready_pipe_write); return ipc; } @@ -588,8 +576,8 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) close(ipc->ready_pipe[1]); } - if (!ipc->is_daemon && ipc->shm_name[0]) { - shm_unlink(ipc->shm_name); + if (!ipc->is_daemon && ipc->shm_path[0]) { + unlink(ipc->shm_path); } sentry_free(ipc); diff --git a/src/backends/native/sentry_crash_ipc.h b/src/backends/native/sentry_crash_ipc.h index 931377327d..19216d965f 100644 --- a/src/backends/native/sentry_crash_ipc.h +++ b/src/backends/native/sentry_crash_ipc.h @@ -9,8 +9,8 @@ # include # include #elif defined(SENTRY_PLATFORM_MACOS) +# include "sentry_sync.h" # include -# include # include #elif defined(SENTRY_PLATFORM_WINDOWS) # include @@ -39,9 +39,9 @@ typedef struct { int shm_fd; int notify_pipe[2]; // Pipe for crash notifications (fork-safe) int ready_pipe[2]; // Pipe for daemon ready signal (fork-safe) - char shm_name[SENTRY_CRASH_IPC_NAME_SIZE]; - sem_t *init_sem; // Named semaphore for initialization synchronization - char sem_name[SENTRY_CRASH_IPC_NAME_SIZE]; + char shm_path[SENTRY_CRASH_MAX_PATH]; // File-backed shm path (sandbox-safe) + sentry_mutex_t + *init_mutex; // Process-wide mutex (sandbox-safe, no sem_open) #elif defined(SENTRY_PLATFORM_WINDOWS) HANDLE shm_handle; HANDLE event_handle; // Event for crash notifications (parent -> daemon) @@ -64,9 +64,10 @@ typedef struct { * @param init_mutex Optional mutex for synchronizing init on Windows (can be * NULL) */ -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) \ - || defined(SENTRY_PLATFORM_MACOS) +#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) sentry_crash_ipc_t *sentry__crash_ipc_init_app(sem_t *init_sem); +#elif defined(SENTRY_PLATFORM_MACOS) +sentry_crash_ipc_t *sentry__crash_ipc_init_app(sentry_mutex_t *init_mutex); #elif defined(SENTRY_PLATFORM_WINDOWS) sentry_crash_ipc_t *sentry__crash_ipc_init_app(HANDLE init_mutex); #else @@ -88,7 +89,7 @@ sentry_crash_ipc_t *sentry__crash_ipc_init_daemon( pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd); #elif defined(SENTRY_PLATFORM_MACOS) sentry_crash_ipc_t *sentry__crash_ipc_init_daemon(pid_t app_pid, - uint64_t app_tid, int notify_pipe_read, int ready_pipe_write); + uint64_t app_tid, int notify_pipe_read, int ready_pipe_write, int shm_fd); #elif defined(SENTRY_PLATFORM_WINDOWS) sentry_crash_ipc_t *sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, HANDLE ready_event_handle); diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index e1abe31f33..938681f537 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -41,17 +41,25 @@ // This lives for the entire backend lifetime and is shared across all threads #if defined(SENTRY_PLATFORM_WINDOWS) static HANDLE g_ipc_mutex = NULL; +#elif defined(SENTRY_PLATFORM_MACOS) +// macOS uses a plain pthread mutex instead of named semaphores (sem_open) +// because App Sandbox blocks POSIX named semaphores. +static sentry_mutex_t g_ipc_sync_mutex = SENTRY__MUTEX_INIT; #else # include static sem_t *g_ipc_init_sem = SEM_FAILED; static char g_ipc_sem_name[64] = { 0 }; #endif -// Mutex to protect IPC initialization (POSIX only, not iOS) -#ifdef SENTRY__MUTEX_INIT_DYN +// Mutex to protect IPC initialization (Windows and Linux only, not macOS/iOS) +// macOS uses g_ipc_sync_mutex directly; iOS has no out-of-process daemon. +#if defined(SENTRY_PLATFORM_WINDOWS) \ + || (!defined(SENTRY_PLATFORM_MACOS) && !defined(SENTRY_PLATFORM_IOS)) +# ifdef SENTRY__MUTEX_INIT_DYN SENTRY__MUTEX_INIT_DYN(g_ipc_init_mutex) -#else +# else static sentry_mutex_t g_ipc_init_mutex = SENTRY__MUTEX_INIT; +# endif #endif /** @@ -94,6 +102,10 @@ native_backend_startup( } sentry__mutex_unlock(&g_ipc_init_mutex); +#elif defined(SENTRY_PLATFORM_MACOS) + // macOS uses a plain pthread mutex (no sem_open which is blocked by App + // Sandbox). The mutex is statically initialized - no setup needed. + (void)0; #elif !defined(SENTRY_PLATFORM_IOS) // Create process-wide IPC initialization semaphore (singleton pattern) // Protected by mutex to handle concurrent backend startups @@ -129,6 +141,8 @@ native_backend_startup( state->ipc = sentry__crash_ipc_init_app(g_ipc_mutex); #elif defined(SENTRY_PLATFORM_IOS) state->ipc = sentry__crash_ipc_init_app(NULL); +#elif defined(SENTRY_PLATFORM_MACOS) + state->ipc = sentry__crash_ipc_init_app(&g_ipc_sync_mutex); #else state->ipc = sentry__crash_ipc_init_app(g_ipc_init_sem); #endif @@ -153,6 +167,8 @@ native_backend_startup( return 1; } } +#elif defined(SENTRY_PLATFORM_MACOS) + sentry__mutex_lock(&g_ipc_sync_mutex); #elif !defined(SENTRY_PLATFORM_IOS) if (g_ipc_init_sem && sem_wait(g_ipc_init_sem) < 0) { SENTRY_WARNF("failed to acquire semaphore for context setup: %s", @@ -298,6 +314,8 @@ native_backend_startup( if (g_ipc_mutex) { ReleaseMutex(g_ipc_mutex); } +#elif defined(SENTRY_PLATFORM_MACOS) + sentry__mutex_unlock(&g_ipc_sync_mutex); #elif !defined(SENTRY_PLATFORM_IOS) // Release semaphore after context configuration if (g_ipc_init_sem) { @@ -328,7 +346,7 @@ native_backend_startup( uint64_t tid = (uint64_t)pthread_self(); state->daemon_pid = sentry__crash_daemon_start(getpid(), tid, state->ipc->notify_pipe[0], - state->ipc->ready_pipe[1], daemon_handler_path); + state->ipc->ready_pipe[1], state->ipc->shm_fd, daemon_handler_path); # elif defined(SENTRY_PLATFORM_WINDOWS) uint64_t tid = (uint64_t)GetCurrentThreadId(); state->daemon_pid = sentry__crash_daemon_start(GetCurrentProcessId(), tid, @@ -449,12 +467,13 @@ native_backend_shutdown(sentry_backend_t *backend) // Dump daemon log file for debugging (especially useful in CI) // Use same naming as shared memory to find the correct log file - if (state->ipc && state->ipc->shmem && state->ipc->shm_name[0] != '\0') { + if (state->ipc && state->ipc->shmem) { char log_path[SENTRY_CRASH_MAX_PATH]; int log_path_len = -1; + // Extract the unique ID from the shm name/path to find the daemon log + // Platform-specific: shm_name on Linux/Windows, shm_path on macOS #if defined(SENTRY_PLATFORM_WINDOWS) - // On Windows, shm_name is wchar_t, need to convert to char for printing const wchar_t *shm_id_w = wcsrchr(state->ipc->shm_name, L'-'); if (shm_id_w) { shm_id_w++; // Skip the '-' @@ -484,8 +503,15 @@ native_backend_shutdown(sentry_backend_t *backend) } } #else - // On Unix, shm_name is char - const char *shm_id = strchr(state->ipc->shm_name, '-'); + // On macOS: shm_path = "{tmpdir}/.sentry-shm-{id}" + // On Linux: shm_name = "/s-{id}" + // In both cases, the ID follows the last '-' +# if defined(SENTRY_PLATFORM_MACOS) + const char *shm_id_src = state->ipc->shm_path; +# else + const char *shm_id_src = state->ipc->shm_name; +# endif + const char *shm_id = shm_id_src[0] ? strrchr(shm_id_src, '-') : NULL; if (shm_id) { shm_id++; // Skip the '-' log_path_len = snprintf(log_path, sizeof(log_path), diff --git a/tests/test_integration_macos_sandbox.py b/tests/test_integration_macos_sandbox.py new file mode 100644 index 0000000000..d678e905f4 --- /dev/null +++ b/tests/test_integration_macos_sandbox.py @@ -0,0 +1,294 @@ +""" +Integration tests for the native backend running inside macOS App Sandbox. + +Verifies that the sandbox-safe IPC (file-backed mmap, pthread mutex, +posix_spawn) works correctly when com.apple.security.app-sandbox is enabled. +""" + +import os +import sys +import plistlib +import shutil +import struct +import subprocess +import tempfile +import textwrap + +import pytest + +from . import make_dsn, Envelope +from .assertions import wait_for_file +from .conditions import has_native + +pytestmark = pytest.mark.skipif( + not (has_native and sys.platform == "darwin"), + reason="macOS App Sandbox tests require native backend on macOS", +) + + +def _create_sandbox_app_bundle(tmp_path, exe_name="sentry_example"): + """ + Create a minimal .app bundle with App Sandbox entitlements around + the already-built executable and its companion sentry-crash daemon. + + Returns the path to the executable inside the bundle. + """ + app_dir = os.path.join(str(tmp_path), "SentryTest.app") + contents_dir = os.path.join(app_dir, "Contents") + macos_dir = os.path.join(contents_dir, "MacOS") + os.makedirs(macos_dir, exist_ok=True) + + # Copy executable and sentry-crash into the bundle + src_exe = os.path.join(str(tmp_path), exe_name) + src_daemon = os.path.join(str(tmp_path), "sentry-crash") + dst_exe = os.path.join(macos_dir, exe_name) + dst_daemon = os.path.join(macos_dir, "sentry-crash") + + shutil.copy2(src_exe, dst_exe) + if os.path.exists(src_daemon): + shutil.copy2(src_daemon, dst_daemon) + + # Copy libsentry.dylib if it exists (shared library build) + src_lib = os.path.join(str(tmp_path), "libsentry.dylib") + if os.path.exists(src_lib): + dst_lib = os.path.join(macos_dir, "libsentry.dylib") + shutil.copy2(src_lib, dst_lib) + + # Write minimal Info.plist + info_plist = { + "CFBundleIdentifier": "io.sentry.native.test", + "CFBundleName": "SentryTest", + "CFBundleExecutable": exe_name, + "CFBundleVersion": "1.0", + "CFBundleShortVersionString": "1.0", + "CFBundlePackageType": "APPL", + "LSMinimumSystemVersion": "10.15", + } + with open(os.path.join(contents_dir, "Info.plist"), "wb") as f: + plistlib.dump(info_plist, f) + + # Write entitlements with App Sandbox enabled and outgoing network + entitlements_path = os.path.join(str(tmp_path), "sandbox.entitlements") + entitlements = { + "com.apple.security.app-sandbox": True, + # Allow outgoing network connections (needed for HTTP transport) + "com.apple.security.network.client": True, + } + with open(entitlements_path, "wb") as f: + plistlib.dump(entitlements, f) + + return app_dir, dst_exe, dst_daemon, entitlements_path + + +def _codesign_bundle(app_dir, entitlements_path, exe_name="sentry_example"): + """ + Ad-hoc sign the .app bundle with sandbox entitlements. + + IMPORTANT: Only the main executable gets sandbox entitlements. + Helper binaries (sentry-crash daemon, dylibs) are signed ad-hoc + WITHOUT entitlements. If a helper binary is signed with sandbox + entitlements but lacks a CFBundleIdentifier, macOS will abort it + with SIGTRAP during _libsecinit_appsandbox initialization. + """ + macos_dir = os.path.join(app_dir, "Contents", "MacOS") + + # Sign helper binaries (daemon, libraries) ad-hoc WITHOUT entitlements + for name in os.listdir(macos_dir): + if name == exe_name: + continue # Main exe gets signed with the bundle + path = os.path.join(macos_dir, name) + if os.access(path, os.X_OK) or name.endswith(".dylib"): + subprocess.run( + ["codesign", "--force", "--sign", "-", path], + check=True, + capture_output=True, + ) + + # Sign the bundle (which signs the main executable with entitlements) + subprocess.run( + [ + "codesign", + "--force", + "--sign", + "-", + "--entitlements", + entitlements_path, + app_dir, + ], + check=True, + capture_output=True, + ) + + +def _run_sandboxed(app_dir, exe_name, args, env, expect_failure=False): + """ + Run the sandboxed app bundle's executable directly. + The sandbox is enforced by the kernel based on the code signature's + entitlements. + """ + exe_path = os.path.join(app_dir, "Contents", "MacOS", exe_name) + + # Set DYLD_LIBRARY_PATH so the executable can find libsentry.dylib + # inside the bundle (dyld respects this even in sandbox for ad-hoc signed) + run_env = dict(env) + run_env["DYLD_LIBRARY_PATH"] = os.path.join(app_dir, "Contents", "MacOS") + + result = subprocess.run( + [exe_path] + args, + env=run_env, + capture_output=True, + timeout=30, + ) + + if expect_failure: + assert result.returncode != 0, ( + f"Expected crash but process exited cleanly.\n" + f"stdout: {result.stdout.decode(errors='replace')}\n" + f"stderr: {result.stderr.decode(errors='replace')}" + ) + else: + assert result.returncode == 0, ( + f"Process failed unexpectedly (rc={result.returncode}).\n" + f"stdout: {result.stdout.decode(errors='replace')}\n" + f"stderr: {result.stderr.decode(errors='replace')}" + ) + + return result + + +def test_sandbox_init_succeeds(cmake): + """ + Verify that sentry_init() succeeds inside App Sandbox. + This is the core regression test: before the fix, sem_open and shm_open + would fail with EACCES inside sandbox, causing init to fail. + """ + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"}) + + app_dir, exe, daemon, ent = _create_sandbox_app_bundle(tmp_path) + _codesign_bundle(app_dir, ent) + + # Run with no-setup to test init/shutdown without crashing + result = _run_sandboxed( + app_dir, + "sentry_example", + ["log", "no-setup"], + env=dict(os.environ), + expect_failure=False, + ) + + # Should not contain IPC failure messages + stderr = result.stderr.decode(errors="replace") + assert "failed to open shared memory" not in stderr.lower() + assert "failed to create ipc semaphore" not in stderr.lower() + assert "posix_spawn failed" not in stderr.lower() + + +def test_sandbox_crash_capture(cmake, httpserver): + """ + Full end-to-end: crash inside sandbox -> daemon captures minidump -> + sends envelope to HTTP server. + """ + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"}) + + app_dir, exe, daemon, ent = _create_sandbox_app_bundle(tmp_path) + _codesign_bundle(app_dir, ent) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + + with httpserver.wait(timeout=15) as waiting: + _run_sandboxed( + app_dir, + "sentry_example", + ["log", "stdout", "crash"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + expect_failure=True, + ) + + assert waiting.result, "Crash envelope should be received from sandboxed app" + + +def test_sandbox_minidump_generated(cmake, httpserver): + """ + Verify that a valid minidump is generated inside sandbox. + """ + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"}) + + app_dir, exe, daemon, ent = _create_sandbox_app_bundle(tmp_path) + _codesign_bundle(app_dir, ent) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + + with httpserver.wait(timeout=15) as waiting: + _run_sandboxed( + app_dir, + "sentry_example", + ["log", "stdout", "crash"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + expect_failure=True, + ) + + assert waiting.result + + # Verify envelope contains minidump + assert len(httpserver.log) >= 1 + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + + has_minidump = any( + item.headers.get("type") == "attachment" + and item.headers.get("attachment_type") == "event.minidump" + for item in envelope.items + ) + assert has_minidump, "Sandboxed crash should produce minidump" + + # Verify minidump has correct signature + for item in envelope.items: + if ( + item.headers.get("type") == "attachment" + and item.headers.get("attachment_type") == "event.minidump" + ): + signature = struct.unpack("= 1 + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + + # Should have native stacktrace, not minidump + has_minidump = any( + item.headers.get("type") == "attachment" + and item.headers.get("attachment_type") == "event.minidump" + for item in envelope.items + ) + assert not has_minidump, "Native-only mode should not include minidump" + + event = envelope.get_event() + assert event is not None, "Should have event" + assert "exception" in event + exc = event["exception"]["values"][0] + assert exc["mechanism"]["type"] == "signalhandler" + assert "stacktrace" in exc + assert len(exc["stacktrace"]["frames"]) > 0