From d041b5a8ab8330a0d46c32002da17bf1da3d513f Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Tue, 14 Apr 2026 11:26:29 +0200 Subject: [PATCH 1/7] fix(native): replace sandbox-incompatible IPC primitives on macOS macOS App Sandbox blocks sem_open(), shm_open(), and fork() in sandboxed apps, causing the native backend to fail during init. - Replace sem_open/sem_wait with pthread_mutex_t for IPC synchronization - Replace shm_open with file-backed mmap using $TMPDIR (sandbox-safe) - Replace fork+exec with posix_spawn using POSIX_SPAWN_CLOEXEC_DEFAULT and explicit fd inheritance via posix_spawn_file_actions_addinherit_np - Pass shm_fd to daemon via posix_spawn instead of reopening by name - Add macOS App Sandbox integration tests verifying init, crash capture, minidump generation, and native stacktraces inside a sandboxed .app Co-Authored-By: Claude Opus 4.6 --- src/backends/native/sentry_crash_daemon.c | 153 +++++++---- src/backends/native/sentry_crash_daemon.h | 5 +- src/backends/native/sentry_crash_handler.c | 13 +- src/backends/native/sentry_crash_ipc.c | 135 +++++---- src/backends/native/sentry_crash_ipc.h | 14 +- src/backends/sentry_backend_native.c | 36 ++- tests/test_integration_macos_sandbox.py | 302 +++++++++++++++++++++ 7 files changed, 513 insertions(+), 145 deletions(-) create mode 100644 tests/test_integration_macos_sandbox.py diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 0798c84492..b8221a2ce4 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -3061,8 +3061,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 +3076,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 +3328,92 @@ 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. +# include +# include + + // 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); + + 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 +3421,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 +3429,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; - 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); - } - } - } -# 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; + 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); } } } -# endif } // exec failed - exit with error @@ -3552,13 +3587,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 +3618,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..723bcdbce1 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -691,11 +691,18 @@ 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 + // 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 from shared memory name (format: "/s-XXXXXXXX") + // 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..080b269f8b 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", + 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,16 @@ 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); + "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 +577,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..145bb586d7 100644 --- a/src/backends/native/sentry_crash_ipc.h +++ b/src/backends/native/sentry_crash_ipc.h @@ -10,8 +10,8 @@ # include #elif defined(SENTRY_PLATFORM_MACOS) # include -# include # include +# include "sentry_sync.h" #elif defined(SENTRY_PLATFORM_WINDOWS) # include #endif @@ -39,9 +39,8 @@ 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 +63,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 +88,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..a0b43e3918 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -41,6 +41,10 @@ // 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; @@ -94,6 +98,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 +137,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 +163,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 +310,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) { @@ -326,9 +340,9 @@ native_backend_startup( state->ipc->notify_fd, state->ipc->ready_fd, daemon_handler_path); # elif defined(SENTRY_PLATFORM_MACOS) 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->daemon_pid = sentry__crash_daemon_start(getpid(), tid, + state->ipc->notify_pipe[0], 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 +463,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 +499,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..06bd2115e0 --- /dev/null +++ b/tests/test_integration_macos_sandbox.py @@ -0,0 +1,302 @@ +""" +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 From 6cb8432c970994a80b1e6c6d0b96354a14a2da96 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Tue, 14 Apr 2026 11:31:22 +0200 Subject: [PATCH 2/7] style: fix formatting and add changelog entry Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 1 + src/backends/native/sentry_crash_daemon.c | 18 +++++++++--------- src/backends/native/sentry_crash_handler.c | 12 ++++++------ src/backends/native/sentry_crash_ipc.c | 9 ++++----- src/backends/native/sentry_crash_ipc.h | 5 +++-- src/backends/sentry_backend_native.c | 6 +++--- tests/test_integration_macos_sandbox.py | 16 ++++------------ 7 files changed, 30 insertions(+), 37 deletions(-) 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 b8221a2ce4..18b5617d65 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -3061,8 +3061,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, int shm_fd) +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, @@ -3337,11 +3337,12 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, #endif { #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. -# include + // 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. # include +# include // Resolve daemon path char daemon_path[SENTRY_CRASH_MAX_PATH]; @@ -3394,9 +3395,8 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, posix_spawn_file_actions_addinherit_np(&file_actions, shm_fd); pid_t daemon_pid; - int spawn_result = posix_spawn( - &daemon_pid, daemon_path, &file_actions, &attr, spawn_argv, - *_NSGetEnviron()); + 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); diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index 723bcdbce1..c2ac0c9863 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -692,14 +692,14 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) // Dump daemon log for debugging (uses stdio, safe after page allocator // enabled) // 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) + // 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 +# else const char *shm_id_src = ipc ? ipc->shm_name : ""; -#endif - if (shm_id_src[0] != '\0' && ctx - && ctx->database_path[0] != '\0') { +# 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 = shm_id_src; *p; p++) { diff --git a/src/backends/native/sentry_crash_ipc.c b/src/backends/native/sentry_crash_ipc.c index 080b269f8b..0b596e36ea 100644 --- a/src/backends/native/sentry_crash_ipc.c +++ b/src/backends/native/sentry_crash_ipc.c @@ -367,8 +367,8 @@ sentry__crash_ipc_init_app(sentry_mutex_t *init_mutex) } if (st.st_size != SENTRY_CRASH_SHM_SIZE) { if (ftruncate(ipc->shm_fd, SENTRY_CRASH_SHM_SIZE) < 0) { - SENTRY_WARNF("failed to resize shared memory file: %s", - strerror(errno)); + SENTRY_WARNF( + "failed to resize shared memory file: %s", strerror(errno)); close(ipc->shm_fd); if (ipc->init_mutex) { sentry__mutex_unlock(ipc->init_mutex); @@ -498,9 +498,8 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, ipc->ready_pipe[0] = -1; ipc->ready_pipe[1] = ready_pipe_write; - SENTRY_DEBUGF( - "daemon: attached to crash IPC (shm_fd=%d, notify_pipe=%d, " - "ready_pipe=%d)", + 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; diff --git a/src/backends/native/sentry_crash_ipc.h b/src/backends/native/sentry_crash_ipc.h index 145bb586d7..19216d965f 100644 --- a/src/backends/native/sentry_crash_ipc.h +++ b/src/backends/native/sentry_crash_ipc.h @@ -9,9 +9,9 @@ # include # include #elif defined(SENTRY_PLATFORM_MACOS) +# include "sentry_sync.h" # include # include -# include "sentry_sync.h" #elif defined(SENTRY_PLATFORM_WINDOWS) # include #endif @@ -40,7 +40,8 @@ typedef struct { int notify_pipe[2]; // Pipe for crash notifications (fork-safe) int ready_pipe[2]; // Pipe for daemon ready signal (fork-safe) 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) + 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) diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index a0b43e3918..6c79392fcd 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -340,9 +340,9 @@ native_backend_startup( state->ipc->notify_fd, state->ipc->ready_fd, daemon_handler_path); # elif defined(SENTRY_PLATFORM_MACOS) 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], - state->ipc->shm_fd, daemon_handler_path); + state->daemon_pid + = sentry__crash_daemon_start(getpid(), tid, state->ipc->notify_pipe[0], + 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, diff --git a/tests/test_integration_macos_sandbox.py b/tests/test_integration_macos_sandbox.py index 06bd2115e0..d678e905f4 100644 --- a/tests/test_integration_macos_sandbox.py +++ b/tests/test_integration_macos_sandbox.py @@ -193,9 +193,7 @@ def test_sandbox_crash_capture(cmake, httpserver): 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" - ) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") with httpserver.wait(timeout=15) as waiting: _run_sandboxed( @@ -218,9 +216,7 @@ def test_sandbox_minidump_generated(cmake, httpserver): 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" - ) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") with httpserver.wait(timeout=15) as waiting: _run_sandboxed( @@ -251,9 +247,7 @@ def test_sandbox_minidump_generated(cmake, httpserver): and item.headers.get("attachment_type") == "event.minidump" ): signature = struct.unpack(" Date: Tue, 14 Apr 2026 11:54:04 +0200 Subject: [PATCH 3/7] fix(native): guard g_ipc_init_mutex with platform ifdefs The mutex is only used on Windows and Linux. On macOS, g_ipc_sync_mutex is used instead, so the unguarded declaration caused -Werror,-Wunused-variable on macOS CI builds. Co-Authored-By: Claude Opus 4.6 --- src/backends/sentry_backend_native.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 6c79392fcd..938681f537 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -51,11 +51,15 @@ 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 /** From c237de662f14afbbd483326d21da1cba6bca319e Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Tue, 14 Apr 2026 13:35:12 +0200 Subject: [PATCH 4/7] fix(native): find last hyphen in shm path for log ID extraction The loop had a `break` that stopped at the first '-', which worked for Linux's "/s-{id}" but gave "shm-{id}" on macOS's "{tmpdir}/.sentry-shm-{id}". Removing the break lets it find the last hyphen and extract just the hex ID. Co-Authored-By: Claude Opus 4.6 --- src/backends/native/sentry_crash_handler.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index c2ac0c9863..5418d2a55f 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -705,7 +705,6 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) for (const char *p = shm_id_src; *p; p++) { if (*p == '-') { shm_id = p + 1; - break; } } From 31d08a79aa8c0d3d2e5e6024dfff844027740347 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Tue, 14 Apr 2026 13:50:25 +0200 Subject: [PATCH 5/7] fix(native): open /dev/null on std fds in posix_spawn file actions POSIX_SPAWN_CLOEXEC_DEFAULT closes all fds including stdin/stdout/ stderr. Without valid std fds, the daemon's fopen() for the log file would get fd 0, which is then destroyed by the daemon's own close(STDIN_FILENO). Pre-opening /dev/null on fds 0-2 via spawn file actions ensures the log file gets a safe fd number. Co-Authored-By: Claude Opus 4.6 --- src/backends/native/sentry_crash_daemon.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 18b5617d65..9ec8f627a3 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -3390,6 +3390,16 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, // Explicitly inherit only the fds the daemon needs posix_spawn_file_actions_t file_actions; posix_spawn_file_actions_init(&file_actions); + // 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. + posix_spawn_file_actions_addopen( + &file_actions, STDIN_FILENO, "/dev/null", O_RDONLY, 0); + posix_spawn_file_actions_addopen( + &file_actions, STDOUT_FILENO, "/dev/null", O_WRONLY, 0); + posix_spawn_file_actions_addopen( + &file_actions, STDERR_FILENO, "/dev/null", O_WRONLY, 0); 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); From 011aced27f630d7a31e0dfaf4beac39a25e8a400 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Tue, 14 Apr 2026 14:01:30 +0200 Subject: [PATCH 6/7] fix(native): avoid clobbering IPC fds when opening /dev/null on std fds If the caller closed stdin/stdout/stderr before sentry_init(), the IPC pipe/shm fds could land on fd 0/1/2. The addopen for /dev/null would then overwrite them. Now we skip opening /dev/null on any fd that's already used by an IPC descriptor. Co-Authored-By: Claude Opus 4.6 --- src/backends/native/sentry_crash_daemon.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 9ec8f627a3..7bf25f2388 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -3390,19 +3390,24 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, // 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. - posix_spawn_file_actions_addopen( - &file_actions, STDIN_FILENO, "/dev/null", O_RDONLY, 0); - posix_spawn_file_actions_addopen( - &file_actions, STDOUT_FILENO, "/dev/null", O_WRONLY, 0); - posix_spawn_file_actions_addopen( - &file_actions, STDERR_FILENO, "/dev/null", O_WRONLY, 0); - 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); + // 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, From 0b4df2111a28d780be8455e8b94ecf38cafb4a9b Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Tue, 14 Apr 2026 14:08:47 +0200 Subject: [PATCH 7/7] style(native): move spawn.h and crt_externs.h to file-scope includes Co-Authored-By: Claude Opus 4.6 --- src/backends/native/sentry_crash_daemon.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 7bf25f2388..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 @@ -3341,8 +3343,6 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, // compatibility. posix_spawn is Apple's recommended API and works correctly // in sandboxed processes, unlike fork() which can have issues with sandbox // inheritance. -# include -# include // Resolve daemon path char daemon_path[SENTRY_CRASH_MAX_PATH];