Skip to content

Commit 2a6ec9d

Browse files
mujacicaclaude
andauthored
fix(native): replace sandbox-incompatible IPC primitives on macOS (#1644)
* 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 <noreply@anthropic.com>
1 parent e15c217 commit 2a6ec9d

File tree

8 files changed

+529
-150
lines changed

8 files changed

+529
-150
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Reset client report counters during initialization ([#1632](https://github.com/getsentry/sentry-native/pull/1632))
1212
- macOS: cache VM regions for FP validation in the new unwinder. ([#1634](https://github.com/getsentry/sentry-native/pull/1634))
1313
- 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))
14+
- 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))
1415

1516
## 0.13.6
1617

src/backends/native/sentry_crash_daemon.c

Lines changed: 115 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
# include <sys/wait.h>
4343
# include <unistd.h>
4444
# if defined(SENTRY_PLATFORM_MACOS)
45+
# include <crt_externs.h>
4546
# include <mach-o/dyld.h>
47+
# include <spawn.h>
4648
# endif
4749
#elif defined(SENTRY_PLATFORM_WINDOWS)
4850
# include <dbghelp.h>
@@ -3062,8 +3064,8 @@ sentry__crash_daemon_main(
30623064
pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd)
30633065
#elif defined(SENTRY_PLATFORM_MACOS)
30643066
int
3065-
sentry__crash_daemon_main(
3066-
pid_t app_pid, uint64_t app_tid, int notify_pipe_read, int ready_pipe_write)
3067+
sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, int notify_pipe_read,
3068+
int ready_pipe_write, int shm_fd)
30673069
#elif defined(SENTRY_PLATFORM_WINDOWS)
30683070
int
30693071
sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle,
@@ -3077,7 +3079,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle,
30773079
app_pid, app_tid, notify_eventfd, ready_eventfd);
30783080
#elif defined(SENTRY_PLATFORM_MACOS)
30793081
sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon(
3080-
app_pid, app_tid, notify_pipe_read, ready_pipe_write);
3082+
app_pid, app_tid, notify_pipe_read, ready_pipe_write, shm_fd);
30813083
#elif defined(SENTRY_PLATFORM_WINDOWS)
30823084
sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon(
30833085
app_pid, app_tid, event_handle, ready_event_handle);
@@ -3329,29 +3331,112 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, int notify_eventfd,
33293331
#elif defined(SENTRY_PLATFORM_MACOS)
33303332
pid_t
33313333
sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid,
3332-
int notify_pipe_read, int ready_pipe_write, const char *handler_path)
3334+
int notify_pipe_read, int ready_pipe_write, int shm_fd,
3335+
const char *handler_path)
33333336
#elif defined(SENTRY_PLATFORM_WINDOWS)
33343337
pid_t
33353338
sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle,
33363339
HANDLE ready_event_handle, const char *handler_path)
33373340
#endif
33383341
{
3339-
#if defined(SENTRY_PLATFORM_UNIX)
3340-
// Fork and exec sentry-crash executable
3341-
// Using exec (not just fork) avoids inheriting sanitizer state and is
3342-
// cleaner
3342+
#if defined(SENTRY_PLATFORM_MACOS)
3343+
// macOS: Use posix_spawn instead of fork+exec for App Sandbox
3344+
// compatibility. posix_spawn is Apple's recommended API and works correctly
3345+
// in sandboxed processes, unlike fork() which can have issues with sandbox
3346+
// inheritance.
3347+
3348+
// Resolve daemon path
3349+
char daemon_path[SENTRY_CRASH_MAX_PATH];
3350+
if (handler_path && handler_path[0] != '\0') {
3351+
strncpy(daemon_path, handler_path, sizeof(daemon_path) - 1);
3352+
daemon_path[sizeof(daemon_path) - 1] = '\0';
3353+
} else {
3354+
char exe_path[SENTRY_CRASH_MAX_PATH];
3355+
uint32_t exe_size = sizeof(exe_path);
3356+
if (_NSGetExecutablePath(exe_path, &exe_size) != 0) {
3357+
SENTRY_WARN("Failed to get executable path for daemon");
3358+
return -1;
3359+
}
3360+
const char *slash = strrchr(exe_path, '/');
3361+
if (!slash
3362+
|| (size_t)(slash - exe_path + 1) + strlen("sentry-crash")
3363+
>= sizeof(daemon_path)) {
3364+
SENTRY_WARN("Daemon path too long");
3365+
return -1;
3366+
}
3367+
size_t dir_len = (size_t)(slash - exe_path + 1);
3368+
memcpy(daemon_path, exe_path, dir_len);
3369+
strcpy(daemon_path + dir_len, "sentry-crash");
3370+
}
3371+
3372+
// Build argument strings (6 args: pid, tid, notify_fd, ready_fd, shm_fd)
3373+
char pid_str[32], tid_str[32], notify_str[32], ready_str[32], shm_str[32];
3374+
snprintf(pid_str, sizeof(pid_str), "%d", (int)app_pid);
3375+
snprintf(tid_str, sizeof(tid_str), "%" PRIx64, app_tid);
3376+
snprintf(notify_str, sizeof(notify_str), "%d", notify_pipe_read);
3377+
snprintf(ready_str, sizeof(ready_str), "%d", ready_pipe_write);
3378+
snprintf(shm_str, sizeof(shm_str), "%d", shm_fd);
3379+
3380+
char *spawn_argv[] = { "sentry-crash", pid_str, tid_str, notify_str,
3381+
ready_str, shm_str, NULL };
3382+
3383+
// Set up posix_spawn attributes
3384+
posix_spawnattr_t attr;
3385+
posix_spawnattr_init(&attr);
3386+
// POSIX_SPAWN_SETSID: create new session (like setsid() after fork)
3387+
// POSIX_SPAWN_CLOEXEC_DEFAULT: close all fds except explicitly inherited
3388+
short spawn_flags = POSIX_SPAWN_SETSID | POSIX_SPAWN_CLOEXEC_DEFAULT;
3389+
posix_spawnattr_setflags(&attr, spawn_flags);
3390+
3391+
// Explicitly inherit only the fds the daemon needs
3392+
posix_spawn_file_actions_t file_actions;
3393+
posix_spawn_file_actions_init(&file_actions);
3394+
posix_spawn_file_actions_addinherit_np(&file_actions, notify_pipe_read);
3395+
posix_spawn_file_actions_addinherit_np(&file_actions, ready_pipe_write);
3396+
posix_spawn_file_actions_addinherit_np(&file_actions, shm_fd);
3397+
// Open /dev/null on stdin/stdout/stderr so the daemon starts with valid
3398+
// standard fds. Without this, POSIX_SPAWN_CLOEXEC_DEFAULT closes them,
3399+
// and the first fopen() in the daemon would get fd 0, which the daemon's
3400+
// own close(STDIN_FILENO) would then destroy.
3401+
// Skip if an IPC fd occupies that slot (e.g. caller closed stdin before
3402+
// sentry_init), to avoid clobbering it with /dev/null.
3403+
int std_fds[3] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO };
3404+
int std_modes[3] = { O_RDONLY, O_WRONLY, O_WRONLY };
3405+
for (int i = 0; i < 3; i++) {
3406+
if (std_fds[i] != notify_pipe_read && std_fds[i] != ready_pipe_write
3407+
&& std_fds[i] != shm_fd) {
3408+
posix_spawn_file_actions_addopen(
3409+
&file_actions, std_fds[i], "/dev/null", std_modes[i], 0);
3410+
}
3411+
}
3412+
3413+
pid_t daemon_pid;
3414+
int spawn_result = posix_spawn(&daemon_pid, daemon_path, &file_actions,
3415+
&attr, spawn_argv, *_NSGetEnviron());
3416+
3417+
posix_spawn_file_actions_destroy(&file_actions);
3418+
posix_spawnattr_destroy(&attr);
3419+
3420+
if (spawn_result != 0) {
3421+
SENTRY_WARNF("posix_spawn failed for %s: %s", daemon_path,
3422+
strerror(spawn_result));
3423+
return -1;
3424+
}
3425+
3426+
return daemon_pid;
3427+
3428+
#elif defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID)
3429+
// Linux: Use fork+exec
33433430
pid_t daemon_pid = fork();
33443431

33453432
if (daemon_pid < 0) {
3346-
// Fork failed
33473433
SENTRY_WARN("Failed to fork daemon process");
33483434
return -1;
33493435
} else if (daemon_pid == 0) {
33503436
// Child process - exec sentry-crash
33513437
setsid();
33523438

33533439
// Clear FD_CLOEXEC on notify and ready fds so they survive exec
3354-
# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID)
33553440
int notify_flags = fcntl(notify_eventfd, F_GETFD);
33563441
if (notify_flags != -1) {
33573442
fcntl(notify_eventfd, F_SETFD, notify_flags & ~FD_CLOEXEC);
@@ -3360,73 +3445,38 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle,
33603445
if (ready_flags != -1) {
33613446
fcntl(ready_eventfd, F_SETFD, ready_flags & ~FD_CLOEXEC);
33623447
}
3363-
# elif defined(SENTRY_PLATFORM_MACOS)
3364-
int notify_flags = fcntl(notify_pipe_read, F_GETFD);
3365-
if (notify_flags != -1) {
3366-
fcntl(notify_pipe_read, F_SETFD, notify_flags & ~FD_CLOEXEC);
3367-
}
3368-
int ready_flags = fcntl(ready_pipe_write, F_GETFD);
3369-
if (ready_flags != -1) {
3370-
fcntl(ready_pipe_write, F_SETFD, ready_flags & ~FD_CLOEXEC);
3371-
}
3372-
# endif
33733448

33743449
// Convert arguments to strings for exec
33753450
char pid_str[32], tid_str[32], notify_str[32], ready_str[32];
33763451
snprintf(pid_str, sizeof(pid_str), "%d", (int)app_pid);
33773452
snprintf(tid_str, sizeof(tid_str), "%" PRIx64, app_tid);
3378-
# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID)
33793453
snprintf(notify_str, sizeof(notify_str), "%d", notify_eventfd);
33803454
snprintf(ready_str, sizeof(ready_str), "%d", ready_eventfd);
3381-
# elif defined(SENTRY_PLATFORM_MACOS)
3382-
snprintf(notify_str, sizeof(notify_str), "%d", notify_pipe_read);
3383-
snprintf(ready_str, sizeof(ready_str), "%d", ready_pipe_write);
3384-
# endif
33853455

33863456
char *argv[]
33873457
= { "sentry-crash", pid_str, tid_str, notify_str, ready_str, NULL };
33883458

3389-
// If handler_path was explicitly set via options, use it directly.
3390-
// Otherwise, look for sentry-crash next to the current executable
3391-
// (matching crashpad's behavior). No fallback chain — fail hard so
3392-
// configuration issues are visible.
33933459
if (handler_path && handler_path[0] != '\0') {
33943460
execv(handler_path, argv);
33953461
} else {
33963462
char exe_path[SENTRY_CRASH_MAX_PATH];
3397-
char daemon_path[SENTRY_CRASH_MAX_PATH];
3463+
char daemon_exec_path[SENTRY_CRASH_MAX_PATH];
33983464

3399-
# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID)
34003465
ssize_t exe_len
34013466
= readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
34023467
if (exe_len > 0) {
34033468
exe_path[exe_len] = '\0';
34043469
const char *slash = strrchr(exe_path, '/');
34053470
if (slash) {
3406-
size_t dir_len = slash - exe_path + 1;
3471+
size_t dir_len = (size_t)(slash - exe_path + 1);
34073472
if (dir_len + strlen("sentry-crash")
3408-
< sizeof(daemon_path)) {
3409-
memcpy(daemon_path, exe_path, dir_len);
3410-
strcpy(daemon_path + dir_len, "sentry-crash");
3411-
execv(daemon_path, argv);
3473+
< sizeof(daemon_exec_path)) {
3474+
memcpy(daemon_exec_path, exe_path, dir_len);
3475+
strcpy(daemon_exec_path + dir_len, "sentry-crash");
3476+
execv(daemon_exec_path, argv);
34123477
}
34133478
}
34143479
}
3415-
# elif defined(SENTRY_PLATFORM_MACOS)
3416-
uint32_t exe_size = sizeof(exe_path);
3417-
if (_NSGetExecutablePath(exe_path, &exe_size) == 0) {
3418-
const char *slash = strrchr(exe_path, '/');
3419-
if (slash) {
3420-
size_t dir_len = slash - exe_path + 1;
3421-
if (dir_len + strlen("sentry-crash")
3422-
< sizeof(daemon_path)) {
3423-
memcpy(daemon_path, exe_path, dir_len);
3424-
strcpy(daemon_path + dir_len, "sentry-crash");
3425-
execv(daemon_path, argv);
3426-
}
3427-
}
3428-
}
3429-
# endif
34303480
}
34313481

34323482
// exec failed - exit with error
@@ -3553,13 +3603,24 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle,
35533603
int
35543604
main(int argc, char **argv)
35553605
{
3556-
// Expected arguments: <app_pid> <app_tid> <notify_handle> <ready_handle>
3606+
// Expected arguments:
3607+
// Linux: <app_pid> <app_tid> <notify_handle> <ready_handle>
3608+
// macOS: <app_pid> <app_tid> <notify_handle> <ready_handle> <shm_fd>
3609+
# if defined(SENTRY_PLATFORM_MACOS)
3610+
if (argc < 6) {
3611+
fprintf(stderr,
3612+
"Usage: sentry-crash <app_pid> <app_tid> <notify_pipe> "
3613+
"<ready_pipe> <shm_fd>\n");
3614+
return 1;
3615+
}
3616+
# else
35573617
if (argc < 5) {
35583618
fprintf(stderr,
35593619
"Usage: sentry-crash <app_pid> <app_tid> <notify_handle> "
35603620
"<ready_handle>\n");
35613621
return 1;
35623622
}
3623+
# endif
35633624

35643625
// Parse arguments
35653626
pid_t app_pid = (pid_t)strtoul(argv[1], NULL, 10);
@@ -3573,8 +3634,9 @@ main(int argc, char **argv)
35733634
# elif defined(SENTRY_PLATFORM_MACOS)
35743635
int notify_pipe_read = atoi(argv[3]);
35753636
int ready_pipe_write = atoi(argv[4]);
3637+
int shm_fd_arg = atoi(argv[5]);
35763638
return sentry__crash_daemon_main(
3577-
app_pid, app_tid, notify_pipe_read, ready_pipe_write);
3639+
app_pid, app_tid, notify_pipe_read, ready_pipe_write, shm_fd_arg);
35783640
# elif defined(SENTRY_PLATFORM_WINDOWS)
35793641
unsigned long long event_handle_val = strtoull(argv[3], NULL, 10);
35803642
unsigned long long ready_event_val = strtoull(argv[4], NULL, 10);

src/backends/native/sentry_crash_daemon.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid,
2929
int notify_eventfd, int ready_eventfd, const char *handler_path);
3030
#elif defined(SENTRY_PLATFORM_MACOS)
3131
pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid,
32-
int notify_pipe_read, int ready_pipe_write, const char *handler_path);
32+
int notify_pipe_read, int ready_pipe_write, int shm_fd,
33+
const char *handler_path);
3334
#elif defined(SENTRY_PLATFORM_WINDOWS)
3435
pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid,
3536
HANDLE event_handle, HANDLE ready_event_handle, const char *handler_path);
@@ -48,7 +49,7 @@ int sentry__crash_daemon_main(
4849
pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd);
4950
#elif defined(SENTRY_PLATFORM_MACOS)
5051
int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid,
51-
int notify_pipe_read, int ready_pipe_write);
52+
int notify_pipe_read, int ready_pipe_write, int shm_fd);
5253
#elif defined(SENTRY_PLATFORM_WINDOWS)
5354
int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid,
5455
HANDLE event_handle, HANDLE ready_event_handle);

src/backends/native/sentry_crash_handler.c

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -691,14 +691,20 @@ crash_signal_handler(int signum, siginfo_t *info, void *context)
691691

692692
// Dump daemon log for debugging (uses stdio, safe after page allocator
693693
// enabled)
694-
if (ipc && ipc->shm_name[0] != '\0' && ctx
695-
&& ctx->database_path[0] != '\0') {
696-
// Extract hex ID from shared memory name (format: "/s-XXXXXXXX")
694+
// Extract the shm identifier for log path construction
695+
// macOS: shm_path = "{tmpdir}/.sentry-shm-{id}", Linux: shm_name =
696+
// "/s-{id}"
697+
# if defined(SENTRY_PLATFORM_MACOS)
698+
const char *shm_id_src = ipc ? ipc->shm_path : "";
699+
# else
700+
const char *shm_id_src = ipc ? ipc->shm_name : "";
701+
# endif
702+
if (shm_id_src[0] != '\0' && ctx && ctx->database_path[0] != '\0') {
703+
// Extract hex ID after last '-' in shm name/path
697704
const char *shm_id = NULL;
698-
for (const char *p = ipc->shm_name; *p; p++) {
705+
for (const char *p = shm_id_src; *p; p++) {
699706
if (*p == '-') {
700707
shm_id = p + 1;
701-
break;
702708
}
703709
}
704710

0 commit comments

Comments
 (0)