From d7983aa21057fbe383a6772a865a90c9d5138900 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 15 May 2026 13:04:35 +0200 Subject: [PATCH 01/22] feat(native): Add Android support Support the native crash daemon on Android by using a file-backed shared mapping and passing its fd through daemon startup. Avoid Android-only link issues and include libunwindstack headers for the daemon target. Add Android integration coverage for the native backend that verifies minidump creation on an emulator. Co-Authored-By: OpenAI Codex --- CMakeLists.txt | 5 + src/CMakeLists.txt | 2 +- src/backends/native/sentry_crash_daemon.c | 47 +++- src/backends/native/sentry_crash_daemon.h | 11 +- src/backends/native/sentry_crash_handler.c | 6 +- src/backends/native/sentry_crash_ipc.c | 262 ++++++++++++++++++++- src/backends/native/sentry_crash_ipc.h | 25 +- src/backends/sentry_backend_native.c | 41 ++-- tests/conditions.py | 4 +- tests/test_integration_android.py | 62 +++++ tests/test_integration_native.py | 14 +- 11 files changed, 442 insertions(+), 37 deletions(-) create mode 100644 tests/test_integration_android.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 76b2377197..1f16ba746b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -841,6 +841,11 @@ elseif(SENTRY_BACKEND_NATIVE) ${PROJECT_SOURCE_DIR}/src ${PROJECT_SOURCE_DIR}/src/backends/native ) + if(SENTRY_WITH_LIBUNWINDSTACK) + target_include_directories(sentry-crash PRIVATE + ${PROJECT_SOURCE_DIR}/external/libunwindstack-ndk/include + ) + endif() # Link same libraries as sentry target_link_libraries(sentry-crash PRIVATE ${_SENTRY_PLATFORM_LIBS}) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6086dbaafb..97baa46bfb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -194,7 +194,7 @@ elseif(SENTRY_BACKEND_NATIVE) target_include_directories(sentry PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/backends/native) # Platform-specific libraries for native backend - if(LINUX OR ANDROID) + if(LINUX AND NOT ANDROID) # Linux needs pthread and rt for shared memory target_link_libraries(sentry PRIVATE pthread rt) elseif(APPLE) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index b440aa1d29..002b060160 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -3237,7 +3237,11 @@ daemon_file_logger( fflush(log_file); // Flush immediately to ensure logs are written } -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_ANDROID) +int +sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, int notify_eventfd, + int ready_eventfd, int shm_fd) +#elif defined(SENTRY_PLATFORM_LINUX) int sentry__crash_daemon_main( pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd) @@ -3253,7 +3257,10 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, { // Initialize IPC first (attach to shared memory created by parent) // We need this to get the database path for logging -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_ANDROID) + sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon( + app_pid, app_tid, notify_eventfd, ready_eventfd, shm_fd); +#elif defined(SENTRY_PLATFORM_LINUX) sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon( app_pid, app_tid, notify_eventfd, ready_eventfd); #elif defined(SENTRY_PLATFORM_MACOS) @@ -3532,7 +3539,11 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, return 0; } -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_ANDROID) +pid_t +sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, int notify_eventfd, + int ready_eventfd, int shm_fd, const char *handler_path) +#elif defined(SENTRY_PLATFORM_LINUX) pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd, const char *handler_path) @@ -3653,6 +3664,12 @@ 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); } +# if defined(SENTRY_PLATFORM_ANDROID) + int shm_flags = fcntl(shm_fd, F_GETFD); + if (shm_flags != -1) { + fcntl(shm_fd, F_SETFD, shm_flags & ~FD_CLOEXEC); + } +# endif // Convert arguments to strings for exec char pid_str[32], tid_str[32], notify_str[32], ready_str[32]; @@ -3661,8 +3678,15 @@ sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, snprintf(notify_str, sizeof(notify_str), "%d", notify_eventfd); snprintf(ready_str, sizeof(ready_str), "%d", ready_eventfd); +# if defined(SENTRY_PLATFORM_ANDROID) + char shm_str[32]; + snprintf(shm_str, sizeof(shm_str), "%d", shm_fd); + char *argv[] = { "sentry-crash", pid_str, tid_str, notify_str, + ready_str, shm_str, NULL }; +# else char *argv[] = { "sentry-crash", pid_str, tid_str, notify_str, ready_str, NULL }; +# endif if (!sentry__string_empty(handler_path)) { execv(handler_path, argv); @@ -3830,12 +3854,13 @@ main(int argc, char **argv) { // Expected arguments: // Linux: - // macOS: -# if defined(SENTRY_PLATFORM_MACOS) + // macOS/Android: + // +# if defined(SENTRY_PLATFORM_MACOS) || defined(SENTRY_PLATFORM_ANDROID) if (argc < 6) { fprintf(stderr, - "Usage: sentry-crash " - " \n"); + "Usage: sentry-crash " + " \n"); return 1; } # else @@ -3851,7 +3876,13 @@ main(int argc, char **argv) pid_t app_pid = (pid_t)strtoul(argv[1], NULL, 10); uint64_t app_tid = strtoull(argv[2], NULL, 16); -# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +# if defined(SENTRY_PLATFORM_ANDROID) + int notify_eventfd = atoi(argv[3]); + int ready_eventfd = atoi(argv[4]); + int shm_fd_arg = atoi(argv[5]); + return sentry__crash_daemon_main( + app_pid, app_tid, notify_eventfd, ready_eventfd, shm_fd_arg); +# elif defined(SENTRY_PLATFORM_LINUX) int notify_eventfd = atoi(argv[3]); int ready_eventfd = atoi(argv[4]); return sentry__crash_daemon_main( diff --git a/src/backends/native/sentry_crash_daemon.h b/src/backends/native/sentry_crash_daemon.h index 69c93e9a26..5228b8f8ad 100644 --- a/src/backends/native/sentry_crash_daemon.h +++ b/src/backends/native/sentry_crash_daemon.h @@ -24,7 +24,11 @@ struct sentry_options_s; * @param ready_handle Ready signal handle * @return Daemon PID on success, -1 on failure */ -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_ANDROID) +pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, + int notify_eventfd, int ready_eventfd, int shm_fd, + const char *handler_path); +#elif defined(SENTRY_PLATFORM_LINUX) 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) @@ -44,7 +48,10 @@ pid_t sentry__crash_daemon_start(pid_t app_pid, uint64_t app_tid, * @param notify_handle Notification handle for crash signals * @param ready_handle Ready signal handle to signal parent */ -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_ANDROID) +int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, + int notify_eventfd, int ready_eventfd, int shm_fd); +#elif defined(SENTRY_PLATFORM_LINUX) int sentry__crash_daemon_main( pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd); #elif defined(SENTRY_PLATFORM_MACOS) diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index 6b6671d452..d39a6936c9 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -693,9 +693,9 @@ 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/Android: shm_path = "{tmpdir}/.sentry-shm-{id}", Linux: + // shm_name = "/s-{id}" +# if defined(SENTRY_PLATFORM_MACOS) || defined(SENTRY_PLATFORM_ANDROID) const char *shm_id_src = ipc ? ipc->shm_path : ""; # else const char *shm_id_src = ipc ? ipc->shm_name : ""; diff --git a/src/backends/native/sentry_crash_ipc.c b/src/backends/native/sentry_crash_ipc.c index c929a07d5c..b73fde40d2 100644 --- a/src/backends/native/sentry_crash_ipc.c +++ b/src/backends/native/sentry_crash_ipc.c @@ -7,7 +7,267 @@ #include #include -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_ANDROID) + +# include +# include +# include +# include +# include +# include + +sentry_crash_ipc_t * +sentry__crash_ipc_init_app( + const char *database_path, 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_mutex = init_mutex; + ipc->shm_fd = -1; + ipc->notify_fd = -1; + ipc->ready_fd = -1; + + uint64_t tid = (uint64_t)pthread_self(); + uint32_t id = (uint32_t)((getpid() ^ (tid & 0xFFFFFFFF)) & 0xFFFFFFFF); + const char *shm_dir = database_path; + if (!shm_dir || !shm_dir[0]) { + shm_dir = getenv("TMPDIR"); + } + if (!shm_dir || !shm_dir[0]) { + shm_dir = "/data/local/tmp"; + } + snprintf(ipc->shm_path, sizeof(ipc->shm_path), "%s/.sentry-shm-%08x", + shm_dir, id); + + if (ipc->init_mutex) { + sentry__mutex_lock(ipc->init_mutex); + } + + bool shm_exists = false; + ipc->shm_fd + = open(ipc->shm_path, O_CREAT | O_RDWR | O_EXCL | O_CLOEXEC, 0600); + if (ipc->shm_fd < 0 && errno == EEXIST) { + shm_exists = true; + ipc->shm_fd = open(ipc->shm_path, O_RDWR | O_CLOEXEC); + } + + if (ipc->shm_fd < 0) { + SENTRY_WARNF("failed to open shared memory file: %s", strerror(errno)); + goto fail; + } + + if (shm_exists) { + struct stat st; + if (fstat(ipc->shm_fd, &st) < 0) { + SENTRY_WARNF( + "failed to stat shared memory file: %s", strerror(errno)); + goto fail; + } + if (st.st_size != SENTRY_CRASH_SHM_SIZE + && ftruncate(ipc->shm_fd, SENTRY_CRASH_SHM_SIZE) < 0) { + SENTRY_WARNF("failed to resize existing shared memory file: %s", + strerror(errno)); + goto fail; + } + } else if (ftruncate(ipc->shm_fd, SENTRY_CRASH_SHM_SIZE) < 0) { + SENTRY_WARNF( + "failed to resize shared memory file: %s", strerror(errno)); + goto fail; + } + + ipc->shmem = mmap(NULL, SENTRY_CRASH_SHM_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED, ipc->shm_fd, 0); + if (ipc->shmem == MAP_FAILED) { + SENTRY_WARNF("failed to map shared memory file: %s", strerror(errno)); + ipc->shmem = NULL; + goto fail; + } + + ipc->notify_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (ipc->notify_fd < 0) { + SENTRY_WARNF("failed to create eventfd: %s", strerror(errno)); + goto fail; + } + + ipc->ready_fd = eventfd(0, EFD_CLOEXEC); + if (ipc->ready_fd < 0) { + SENTRY_WARNF("failed to create ready eventfd: %s", strerror(errno)); + goto fail; + } + + if (!shm_exists) { + memset(ipc->shmem, 0, SENTRY_CRASH_SHM_SIZE); + ipc->shmem->magic = SENTRY_CRASH_MAGIC; + ipc->shmem->version = SENTRY_CRASH_VERSION; + sentry__atomic_store(&ipc->shmem->state, SENTRY_CRASH_STATE_READY); + sentry__atomic_store(&ipc->shmem->sequence, 0); + } + + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); + } + + SENTRY_DEBUGF("initialized crash IPC (shm=%s, notify_fd=%d)", ipc->shm_path, + ipc->notify_fd); + + return ipc; + +fail: + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); + } + if (ipc->shmem) { + munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); + } + if (ipc->notify_fd >= 0) { + close(ipc->notify_fd); + } + if (ipc->ready_fd >= 0) { + close(ipc->ready_fd); + } + if (ipc->shm_fd >= 0) { + close(ipc->shm_fd); + } + if (!shm_exists && ipc->shm_path[0]) { + unlink(ipc->shm_path); + } + sentry_free(ipc); + return NULL; +} + +sentry_crash_ipc_t * +sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, + int notify_eventfd, int ready_eventfd, 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; + ipc->shm_fd = shm_fd; + ipc->notify_fd = notify_eventfd; + ipc->ready_fd = ready_eventfd; + + ipc->shmem = mmap(NULL, SENTRY_CRASH_SHM_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED, ipc->shm_fd, 0); + if (ipc->shmem == MAP_FAILED) { + SENTRY_WARNF( + "daemon: failed to map shared memory file: %s", strerror(errno)); + ipc->shmem = NULL; + close(ipc->shm_fd); + sentry_free(ipc); + return NULL; + } + + if (ipc->shmem->magic != SENTRY_CRASH_MAGIC) { + SENTRY_WARN("daemon: invalid shared memory magic"); + munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); + close(ipc->shm_fd); + sentry_free(ipc); + return NULL; + } + + if (ipc->shmem->database_path[0]) { + uint32_t id + = (uint32_t)((app_pid ^ (app_tid & 0xFFFFFFFF)) & 0xFFFFFFFF); + snprintf(ipc->shm_path, sizeof(ipc->shm_path), "%s/.sentry-shm-%08x", + ipc->shmem->database_path, id); + } + + SENTRY_DEBUGF("daemon: attached to crash IPC (shm_fd=%d, notify_fd=%d, " + "ready_notify_fd=%d)", + shm_fd, notify_eventfd, ready_eventfd); + + return ipc; +} + +void +sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc) +{ + if (!ipc || ipc->notify_fd < 0) { + return; + } + + uint64_t val = 1; + ssize_t written = write(ipc->notify_fd, &val, sizeof(val)); + (void)written; +} + +bool +sentry__crash_ipc_wait(sentry_crash_ipc_t *ipc, int timeout_ms) +{ + if (!ipc || ipc->notify_fd < 0) { + return false; + } + + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(ipc->notify_fd, &readfds); + + struct timeval timeout; + timeout.tv_sec = timeout_ms / 1000; + timeout.tv_usec = (timeout_ms % 1000) * 1000; + + int ret = select(ipc->notify_fd + 1, &readfds, NULL, NULL, + timeout_ms >= 0 ? &timeout : NULL); + + if (ret > 0 && FD_ISSET(ipc->notify_fd, &readfds)) { + uint64_t val; + ssize_t result = read(ipc->notify_fd, &val, sizeof(val)); + if (result < 0) { + SENTRY_WARN("Failed to read from notify_fd"); + } + return true; + } + + return false; +} + +void +sentry__crash_ipc_unlink(sentry_crash_ipc_t *ipc) +{ + if (ipc && ipc->shm_path[0]) { + unlink(ipc->shm_path); + } +} + +void +sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) +{ + if (!ipc) { + return; + } + + if (ipc->shmem) { + munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); + } + + if (ipc->shm_fd >= 0) { + close(ipc->shm_fd); + } + + if (!ipc->is_daemon) { + sentry__crash_ipc_unlink(ipc); + } + + if (ipc->notify_fd >= 0) { + close(ipc->notify_fd); + } + + if (ipc->ready_fd >= 0) { + close(ipc->ready_fd); + } + + sentry_free(ipc); +} + +#elif defined(SENTRY_PLATFORM_LINUX) # include # include diff --git a/src/backends/native/sentry_crash_ipc.h b/src/backends/native/sentry_crash_ipc.h index ad0e4fa6fe..6c075951a2 100644 --- a/src/backends/native/sentry_crash_ipc.h +++ b/src/backends/native/sentry_crash_ipc.h @@ -4,7 +4,11 @@ #include "sentry_boot.h" #include "sentry_crash_context.h" -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_ANDROID) +# include "sentry_sync.h" +# include +# include +#elif defined(SENTRY_PLATFORM_LINUX) # include # include # include @@ -28,7 +32,13 @@ typedef pid_t sentry_process_handle_t; typedef struct { sentry_crash_context_t *shmem; -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_ANDROID) + int shm_fd; + int notify_fd; // Eventfd for crash notifications + int ready_fd; // Eventfd for daemon ready signal + char shm_path[SENTRY_CRASH_MAX_PATH]; // File-backed shm path + sentry_mutex_t *init_mutex; // Process-wide initialization mutex +#elif defined(SENTRY_PLATFORM_LINUX) int shm_fd; int notify_fd; // Eventfd for crash notifications int ready_fd; // Eventfd for daemon ready signal @@ -60,11 +70,15 @@ typedef struct { /** * Initialize IPC for application process. * Creates shared memory and notification mechanism. + * @param database_path Android-only directory for the file-backed mapping * @param init_sem Optional semaphore for synchronizing init (can be NULL) * @param init_mutex Optional mutex for synchronizing init on Windows (can be * NULL) */ -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_ANDROID) +sentry_crash_ipc_t *sentry__crash_ipc_init_app( + const char *database_path, sentry_mutex_t *init_mutex); +#elif defined(SENTRY_PLATFORM_LINUX) 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); @@ -84,7 +98,10 @@ sentry_crash_ipc_t *sentry__crash_ipc_init_app(void); * @param ready_handle Ready signal handle inherited from parent (eventfd on * Linux, pipe fd on macOS, event on Windows) */ -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +#if defined(SENTRY_PLATFORM_ANDROID) +sentry_crash_ipc_t *sentry__crash_ipc_init_daemon(pid_t app_pid, + uint64_t app_tid, int notify_eventfd, int ready_eventfd, int shm_fd); +#elif defined(SENTRY_PLATFORM_LINUX) 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) diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index fe1af93226..e8cd5a8b2f 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -44,9 +44,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. +#elif defined(SENTRY_PLATFORM_MACOS) || defined(SENTRY_PLATFORM_ANDROID) +// macOS/Android use a plain pthread mutex instead of named semaphores +// (sem_open) because App Sandbox blocks POSIX named semaphores and Android has +// no shm_open. static sentry_mutex_t g_ipc_sync_mutex = SENTRY__MUTEX_INIT; #else # include @@ -54,10 +55,12 @@ static sem_t *g_ipc_init_sem = SEM_FAILED; static char g_ipc_sem_name[64] = { 0 }; #endif -// 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. +// Mutex to protect IPC initialization (Windows and Linux only, not macOS/iOS or +// Android) macOS/Android use 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)) + || (!defined(SENTRY_PLATFORM_MACOS) && !defined(SENTRY_PLATFORM_ANDROID) \ + && !defined(SENTRY_PLATFORM_IOS)) # ifdef SENTRY__MUTEX_INIT_DYN SENTRY__MUTEX_INIT_DYN(g_ipc_init_mutex) # else @@ -208,9 +211,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. +#elif defined(SENTRY_PLATFORM_MACOS) || defined(SENTRY_PLATFORM_ANDROID) + // macOS/Android use a plain pthread mutex (no sem_open/shm_open which is + // blocked by App Sandbox, or missing on Android). The mutex is statically + // initialized - no setup needed. (void)0; #elif !defined(SENTRY_PLATFORM_IOS) // Create process-wide IPC initialization semaphore (singleton pattern) @@ -247,6 +251,10 @@ 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_ANDROID) + state->ipc = sentry__crash_ipc_init_app( + options->database_path ? options->database_path->path : NULL, + &g_ipc_sync_mutex); #elif defined(SENTRY_PLATFORM_MACOS) state->ipc = sentry__crash_ipc_init_app(&g_ipc_sync_mutex); #else @@ -273,7 +281,7 @@ native_backend_startup( return 1; } } -#elif defined(SENTRY_PLATFORM_MACOS) +#elif defined(SENTRY_PLATFORM_MACOS) || defined(SENTRY_PLATFORM_ANDROID) sentry__mutex_lock(&g_ipc_sync_mutex); #elif !defined(SENTRY_PLATFORM_IOS) if (g_ipc_init_sem && sem_wait(g_ipc_init_sem) < 0) { @@ -428,7 +436,7 @@ native_backend_startup( if (g_ipc_mutex) { ReleaseMutex(g_ipc_mutex); } -#elif defined(SENTRY_PLATFORM_MACOS) +#elif defined(SENTRY_PLATFORM_MACOS) || defined(SENTRY_PLATFORM_ANDROID) sentry__mutex_unlock(&g_ipc_sync_mutex); #elif !defined(SENTRY_PLATFORM_IOS) // Release semaphore after context configuration @@ -452,7 +460,12 @@ native_backend_startup( // Pass the notification handles (eventfd/pipe on Unix, events on Windows) const char *daemon_handler_path = options->handler_path ? options->handler_path->path : NULL; -# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +# if defined(SENTRY_PLATFORM_ANDROID) + uint64_t tid = (uint64_t)pthread_self(); + state->daemon_pid + = sentry__crash_daemon_start(getpid(), tid, state->ipc->notify_fd, + state->ipc->ready_fd, state->ipc->shm_fd, daemon_handler_path); +# elif defined(SENTRY_PLATFORM_LINUX) uint64_t tid = (uint64_t)pthread_self(); state->daemon_pid = sentry__crash_daemon_start(getpid(), tid, state->ipc->notify_fd, state->ipc->ready_fd, daemon_handler_path); @@ -628,10 +641,10 @@ native_backend_shutdown(sentry_backend_t *backend) } } #else - // On macOS: shm_path = "{tmpdir}/.sentry-shm-{id}" + // On macOS/Android: 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) +# if defined(SENTRY_PLATFORM_MACOS) || defined(SENTRY_PLATFORM_ANDROID) const char *shm_id_src = state->ipc->shm_path; # else const char *shm_id_src = state->ipc->shm_name; diff --git a/tests/conditions.py b/tests/conditions.py index 4e10e4c350..4b9a6531a8 100644 --- a/tests/conditions.py +++ b/tests/conditions.py @@ -50,6 +50,8 @@ # Native backend works on all platforms (lightweight, no external dependencies) # It's always available - tests explicitly set SENTRY_BACKEND: native in cmake # On macOS ASAN, the signal handling conflicts with ASAN's memory interception -has_native = has_http and not (is_asan and sys.platform == "darwin") +has_native = not (is_asan and sys.platform == "darwin") and ( + not is_android or int(is_android) >= 23 +) has_sccache = os.environ.get("USE_SCCACHE") and shutil.which("sccache") diff --git a/tests/test_integration_android.py b/tests/test_integration_android.py new file mode 100644 index 0000000000..0fdef5cbe5 --- /dev/null +++ b/tests/test_integration_android.py @@ -0,0 +1,62 @@ +import time + +import pytest + +from . import adb, run +from .conditions import has_native, is_android + +pytestmark = pytest.mark.skipif( + not is_android, + reason="Tests need Android", +) + + +@pytest.mark.skipif( + not has_native, + reason="Tests need the native backend enabled", +) +@pytest.mark.skipif( + is_android and int(is_android) < 23, + reason="Android native backend test needs API 23+ (process_vm_readv)", +) +def test_native_android(cmake): + database_path = "/data/local/tmp/.sentry-native" + + def find_minidumps(timeout=10): + deadline = time.time() + timeout + last_result = None + while last_result is None or time.time() < deadline: + last_result = adb( + "shell", + f'for f in {database_path}/*.dmp; do [ -f "$f" ] && echo "$f"; done', + capture_output=True, + text=True, + check=False, + ) + if last_result.returncode == 0 and last_result.stdout.strip(): + return [ + line.strip() + for line in last_result.stdout.splitlines() + if line.strip() + ] + time.sleep(0.5) + if last_result is not None: + print(last_result.stdout) + print(last_result.stderr) + return [] + + tmp_path = cmake( + ["sentry_example"], {"SENTRY_BACKEND": "native", "SENTRY_TRANSPORT": "none"} + ) + + adb("shell", f"rm -rf {database_path}", check=True) + assert find_minidumps(timeout=0) == [] + + run(tmp_path, "sentry_example", ["log", "crash"], expect_failure=True) + + minidumps = find_minidumps() + assert minidumps, "native backend should create a minidump on Android" + + local_minidump = tmp_path / "android.dmp" + adb("pull", minidumps[0], str(local_minidump), check=True, capture_output=True) + assert local_minidump.stat().st_size > 0 diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 6d0c697b7a..a3563fd7da 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -26,11 +26,19 @@ wait_for_file, assert_user_feedback, ) -from .conditions import has_native, has_oom, is_kcov, is_asan, is_tsan, is_qemu +from .conditions import ( + has_http, + has_native, + has_oom, + is_kcov, + is_asan, + is_tsan, + is_qemu, +) pytestmark = pytest.mark.skipif( - not has_native or is_qemu, - reason="Tests need the native backend enabled", + not has_native or not has_http or is_qemu, + reason="Tests need the native backend and HTTP support", ) # Sanitizer builds are slower, so selected native crash tests use the same 10s From 92d076e153fbc4b5dc1e074b4606c0cb4eaaea9a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 15 May 2026 14:23:51 +0200 Subject: [PATCH 02/22] fix(native): Avoid leaking Android daemon IPC fds Assign inherited notification fds only after daemon shared memory mapping and validation succeed so failed Android daemon initialization does not retain them. Co-Authored-By: OpenAI Codex --- src/backends/native/sentry_crash_ipc.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/backends/native/sentry_crash_ipc.c b/src/backends/native/sentry_crash_ipc.c index b73fde40d2..6200bcb2eb 100644 --- a/src/backends/native/sentry_crash_ipc.c +++ b/src/backends/native/sentry_crash_ipc.c @@ -151,8 +151,6 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, } ipc->is_daemon = true; ipc->shm_fd = shm_fd; - ipc->notify_fd = notify_eventfd; - ipc->ready_fd = ready_eventfd; ipc->shmem = mmap(NULL, SENTRY_CRASH_SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, ipc->shm_fd, 0); @@ -180,6 +178,9 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, ipc->shmem->database_path, id); } + ipc->notify_fd = notify_eventfd; + ipc->ready_fd = ready_eventfd; + SENTRY_DEBUGF("daemon: attached to crash IPC (shm_fd=%d, notify_fd=%d, " "ready_notify_fd=%d)", shm_fd, notify_eventfd, ready_eventfd); From 25beed6a96154f438ce5be0e8ca971ad698dd9e0 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 15 May 2026 14:33:53 +0200 Subject: [PATCH 03/22] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f19a4bc2fc..5e6f2992e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +**Features**: + +- Native: add Android support. ([#1725](https://github.com/getsentry/sentry-native/pull/1725)) + **Fixes**: - Protect CMAKE_SYSTEM_VERSION to avoid empty values when cross-building. ([#1720](https://github.com/getsentry/sentry-native/pull/1720)) From bdb7cbfc0487fc926e0c10d2ae23f9649ca51e6e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 15 May 2026 14:36:06 +0200 Subject: [PATCH 04/22] Fix skipifs --- tests/test_integration_android.py | 2 +- tests/test_integration_logger.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_integration_android.py b/tests/test_integration_android.py index 0fdef5cbe5..77010a7e10 100644 --- a/tests/test_integration_android.py +++ b/tests/test_integration_android.py @@ -16,7 +16,7 @@ reason="Tests need the native backend enabled", ) @pytest.mark.skipif( - is_android and int(is_android) < 23, + bool(is_android) and int(is_android) < 23, reason="Android native backend test needs API 23+ (process_vm_readv)", ) def test_native_android(cmake): diff --git a/tests/test_integration_logger.py b/tests/test_integration_logger.py index 43fe801493..7a17dbb500 100644 --- a/tests/test_integration_logger.py +++ b/tests/test_integration_logger.py @@ -127,7 +127,8 @@ def parse_logger_output(output): "native", marks=[ pytest.mark.skipif( - not has_native or is_qemu, reason="native backend not available" + not has_native or is_qemu or is_android, + reason="native backend not available", ), ], ), @@ -175,7 +176,8 @@ def test_logger_enabled_when_crashed(backend, cmake): "native", marks=[ pytest.mark.skipif( - not has_native or is_qemu, reason="native backend not available" + not has_native or is_qemu or is_android, + reason="native backend not available", ), ], ), From b9840301c9d41198d3b63d8bc4aebc9bbfb5dd75 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 15 May 2026 14:39:55 +0200 Subject: [PATCH 05/22] fix(native): Remove stale Android daemon parameter casts app_pid and app_tid are used to reconstruct the Android crash IPC shared memory path, so do not mark them as unused. Co-Authored-By: OpenAI Codex --- src/backends/native/sentry_crash_ipc.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/backends/native/sentry_crash_ipc.c b/src/backends/native/sentry_crash_ipc.c index 6200bcb2eb..4dada9b054 100644 --- a/src/backends/native/sentry_crash_ipc.c +++ b/src/backends/native/sentry_crash_ipc.c @@ -142,9 +142,6 @@ sentry_crash_ipc_t * sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd, int shm_fd) { - (void)app_pid; - (void)app_tid; - sentry_crash_ipc_t *ipc = SENTRY_MAKE(sentry_crash_ipc_t); if (!ipc) { return NULL; From 0930ddb5759da4ea7b77996e55248f7e9855d6f8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 15 May 2026 14:56:24 +0200 Subject: [PATCH 06/22] default is_android to 0 instead of None --- tests/conditions.py | 4 ++-- tests/test_integration_android.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conditions.py b/tests/conditions.py index 4b9a6531a8..aff62cb375 100644 --- a/tests/conditions.py +++ b/tests/conditions.py @@ -3,7 +3,7 @@ import shutil is_aix = sys.platform == "aix" or sys.platform == "os400" -is_android = os.environ.get("ANDROID_API") +is_android = int(os.environ.get("ANDROID_API", "0")) is_x86 = os.environ.get("TEST_X86") is_arm32 = bool(os.environ.get("TEST_ARM32")) is_qemu = bool(os.environ.get("TEST_QEMU")) @@ -51,7 +51,7 @@ # It's always available - tests explicitly set SENTRY_BACKEND: native in cmake # On macOS ASAN, the signal handling conflicts with ASAN's memory interception has_native = not (is_asan and sys.platform == "darwin") and ( - not is_android or int(is_android) >= 23 + not is_android or is_android >= 23 ) has_sccache = os.environ.get("USE_SCCACHE") and shutil.which("sccache") diff --git a/tests/test_integration_android.py b/tests/test_integration_android.py index 77010a7e10..2ac8f4ebde 100644 --- a/tests/test_integration_android.py +++ b/tests/test_integration_android.py @@ -16,7 +16,7 @@ reason="Tests need the native backend enabled", ) @pytest.mark.skipif( - bool(is_android) and int(is_android) < 23, + 0 < is_android < 23, reason="Android native backend test needs API 23+ (process_vm_readv)", ) def test_native_android(cmake): From 2c9a5d96b07ce95b3204d53a8977a11bbd8662e7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 15 May 2026 15:58:12 +0200 Subject: [PATCH 07/22] fix(native): Require Android IPC database path Use the configured database path for Android crash IPC shared memory files instead of falling back to shell temp locations. Co-Authored-By: OpenAI Codex --- src/backends/native/sentry_crash_ipc.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/backends/native/sentry_crash_ipc.c b/src/backends/native/sentry_crash_ipc.c index 4dada9b054..f404006e39 100644 --- a/src/backends/native/sentry_crash_ipc.c +++ b/src/backends/native/sentry_crash_ipc.c @@ -20,6 +20,11 @@ sentry_crash_ipc_t * sentry__crash_ipc_init_app( const char *database_path, sentry_mutex_t *init_mutex) { + if (!database_path || !database_path[0]) { + SENTRY_WARN("Android crash IPC requires a database path"); + return NULL; + } + sentry_crash_ipc_t *ipc = SENTRY_MAKE(sentry_crash_ipc_t); if (!ipc) { return NULL; @@ -32,15 +37,8 @@ sentry__crash_ipc_init_app( uint64_t tid = (uint64_t)pthread_self(); uint32_t id = (uint32_t)((getpid() ^ (tid & 0xFFFFFFFF)) & 0xFFFFFFFF); - const char *shm_dir = database_path; - if (!shm_dir || !shm_dir[0]) { - shm_dir = getenv("TMPDIR"); - } - if (!shm_dir || !shm_dir[0]) { - shm_dir = "/data/local/tmp"; - } snprintf(ipc->shm_path, sizeof(ipc->shm_path), "%s/.sentry-shm-%08x", - shm_dir, id); + database_path, id); if (ipc->init_mutex) { sentry__mutex_lock(ipc->init_mutex); From c4990435d3f44df3aa0f116ab2599febd7b82d1f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 15 May 2026 17:09:52 +0200 Subject: [PATCH 08/22] test: Handle empty Android API in conditions Treat an empty ANDROID_API value the same as an unset one so importing test conditions does not fail before collection. Co-Authored-By: OpenAI Codex --- tests/conditions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conditions.py b/tests/conditions.py index aff62cb375..f11033a014 100644 --- a/tests/conditions.py +++ b/tests/conditions.py @@ -3,7 +3,7 @@ import shutil is_aix = sys.platform == "aix" or sys.platform == "os400" -is_android = int(os.environ.get("ANDROID_API", "0")) +is_android = int(os.environ.get("ANDROID_API") or "0") is_x86 = os.environ.get("TEST_X86") is_arm32 = bool(os.environ.get("TEST_ARM32")) is_qemu = bool(os.environ.get("TEST_QEMU")) From 9c5c7dfc8e910d1be729ec29622fbaf39ee932e1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 15 May 2026 18:16:44 +0200 Subject: [PATCH 09/22] skip mac sandbox tests on android --- tests/test_integration_macos_sandbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_integration_macos_sandbox.py b/tests/test_integration_macos_sandbox.py index d678e905f4..de090b6d9c 100644 --- a/tests/test_integration_macos_sandbox.py +++ b/tests/test_integration_macos_sandbox.py @@ -18,10 +18,10 @@ from . import make_dsn, Envelope from .assertions import wait_for_file -from .conditions import has_native +from .conditions import has_native, is_android pytestmark = pytest.mark.skipif( - not (has_native and sys.platform == "darwin"), + not (has_native and sys.platform == "darwin" and not is_android), reason="macOS App Sandbox tests require native backend on macOS", ) From 03a811ff0da9705ed957a2f262f167bf19f3c6ff Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 19 May 2026 14:44:02 +0200 Subject: [PATCH 10/22] WIP --- ndk/lib/CMakeLists.txt | 18 +++ ndk/lib/build.gradle.kts | 8 +- ndk/lib/src/main/jni/sentry.c | 82 ++++++------ .../src/main/jni/sentry_android_transport.c | 126 ++++++++++++++++++ ndk/sample/CMakeLists.txt | 6 +- ndk/sample/build.gradle.kts | 8 +- .../native/minidump/sentry_minidump_linux.c | 9 +- src/backends/native/sentry_crash_daemon.c | 5 +- src/modulefinder/sentry_modulefinder_linux.c | 15 +-- src/sentry_os.c | 19 +++ src/sentry_os.h | 11 ++ 11 files changed, 236 insertions(+), 71 deletions(-) create mode 100644 ndk/lib/src/main/jni/sentry_android_transport.c diff --git a/ndk/lib/CMakeLists.txt b/ndk/lib/CMakeLists.txt index 9bef5d7b3b..e7bf8e4017 100644 --- a/ndk/lib/CMakeLists.txt +++ b/ndk/lib/CMakeLists.txt @@ -12,9 +12,27 @@ endif() # make sure that we build it as a shared lib instead of a static lib set(BUILD_SHARED_LIBS ON) set(SENTRY_BUILD_SHARED_LIBS ON) +set(SENTRY_TRANSPORT "custom" CACHE STRING "" FORCE) # Adding sentry-native project add_subdirectory(${SENTRY_NATIVE_SRC} sentry_build) +target_sources(sentry PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jni/sentry_android_transport.c +) + +if(SENTRY_BACKEND STREQUAL "native") + target_sources(sentry-crash PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jni/sentry_android_transport.c + ) + target_compile_definitions(sentry-android PRIVATE SENTRY_BACKEND_NATIVE) + set_target_properties(sentry-crash PROPERTIES + PREFIX "lib" + OUTPUT_NAME "sentry-crash" + SUFFIX ".so" + ) + target_link_options(sentry-crash PRIVATE "-Wl,-z,max-page-size=16384") + target_link_libraries(sentry-android PRIVATE dl) +endif() # Android logging library find_library(LOG_LIB log) diff --git a/ndk/lib/build.gradle.kts b/ndk/lib/build.gradle.kts index 73708d9dc7..91a95f1e4a 100644 --- a/ndk/lib/build.gradle.kts +++ b/ndk/lib/build.gradle.kts @@ -5,6 +5,7 @@ plugins { } var sentryNativeSrc: String = "${project.projectDir}/../.." +val sentryBackend = providers.gradleProperty("sentryBackend").orElse("inproc").get() android { compileSdk = 35 @@ -18,6 +19,7 @@ android { externalNativeBuild { cmake { arguments.add(0, "-DANDROID_STL=c++_static") + arguments.add(0, "-DSENTRY_BACKEND=$sentryBackend") arguments.add(0, "-DSENTRY_NATIVE_SRC=$sentryNativeSrc") } } @@ -137,10 +139,12 @@ dependencies { * */ afterEvaluate { - tasks.getByName("prefabReleasePackage") { + tasks.matching { it.name.startsWith("prefab") && it.name.endsWith("Package") }.configureEach { doLast { - project.fileTree("build/intermediates/prefab_package/") { + project.fileTree("build/intermediates/") { include("**/abi.json") + include("**/prefab_publication.json/debug") + include("**/prefab_publication.json/release") }.forEach { file -> file.writeText(file.readText().replace("c++_static", "none")) } diff --git a/ndk/lib/src/main/jni/sentry.c b/ndk/lib/src/main/jni/sentry.c index c64f005655..060ee66df8 100644 --- a/ndk/lib/src/main/jni/sentry.c +++ b/ndk/lib/src/main/jni/sentry.c @@ -1,12 +1,16 @@ #include #include -#include #include #include -#define ENSURE(Expr) \ - if (!(Expr)) \ - return +#if defined(SENTRY_BACKEND_NATIVE) +#include +#include + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif +#endif #define ENSURE_OR_FAIL(Expr) \ if (!(Expr)) \ @@ -59,6 +63,31 @@ static char *call_get_string(JNIEnv *env, jobject obj, jmethodID mid) { return NULL; } +#if defined(SENTRY_BACKEND_NATIVE) +static void set_handler_path(sentry_options_t *options) { + Dl_info info; + if (dladdr((void *)set_handler_path, &info) == 0 || !info.dli_fname) { + return; + } + + const char *slash = strrchr(info.dli_fname, '/'); + if (!slash) { + return; + } + + const char daemon_name[] = "libsentry-crash.so"; + size_t dir_len = (size_t)(slash - info.dli_fname + 1); + if (dir_len + sizeof(daemon_name) > PATH_MAX) { + return; + } + + char handler_path[PATH_MAX]; + memcpy(handler_path, info.dli_fname, dir_len); + memcpy(handler_path + dir_len, daemon_name, sizeof(daemon_name)); + sentry_options_set_handler_path(options, handler_path); +} +#endif + JNIEXPORT void JNICALL Java_io_sentry_ndk_NativeScope_nativeSetTag( JNIEnv *env, @@ -282,26 +311,6 @@ Java_io_sentry_ndk_NativeScope_nativeClearAttachments(JNIEnv *env, jclass cls) { sentry_clear_attachments(); } -static void send_envelope(sentry_envelope_t *envelope, void *data) { - const char *outbox_path = (const char *) data; - char envelope_id_str[40]; - - sentry_uuid_t envelope_id = sentry_uuid_new_v4(); - sentry_uuid_as_string(&envelope_id, envelope_id_str); - - size_t outbox_len = strlen(outbox_path); - size_t final_len = outbox_len + 42; // "/" + envelope_id_str + "\0" = 42 - char *envelope_path = sentry_malloc(final_len); - ENSURE(envelope_path); - int written = snprintf(envelope_path, final_len, "%s/%s", outbox_path, envelope_id_str); - if (written > outbox_len && written < final_len) { - sentry_envelope_write_to_file(envelope, envelope_path); - } - - sentry_free(envelope_path); - sentry_envelope_free(envelope); -} - // sentry_backend.h extern void sentry__backend_preload(void); @@ -337,10 +346,7 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( (*env)->DeleteLocalRef(env, options_cls); char *outbox_path = NULL; - sentry_transport_t *transport = NULL; - bool transport_owns_path = false; sentry_options_t *options = NULL; - bool options_owns_transport = false; char *dsn_str = NULL; char *release_str = NULL; char *environment_str = NULL; @@ -350,6 +356,10 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( options = sentry_options_new(); ENSURE_OR_FAIL(options); +#if defined(SENTRY_BACKEND_NATIVE) + set_handler_path(options); +#endif + // session tracking is enabled by default, but the Android SDK already handles it sentry_options_set_auto_session_tracking(options, 0); @@ -362,15 +372,6 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( outbox_path = call_get_string(env, sentry_ndk_options, outbox_path_mid); ENSURE_OR_FAIL(outbox_path); - transport = sentry_transport_new(send_envelope); - ENSURE_OR_FAIL(transport); - sentry_transport_set_state(transport, outbox_path); - sentry_transport_set_free_func(transport, sentry_free); - transport_owns_path = true; - - sentry_options_set_transport(options, transport); - options_owns_transport = true; - // give sentry-native its own database path it can work with, next to the outbox size_t outbox_len = strlen(outbox_path); size_t final_len = outbox_len + 15; // len(".sentry-native\0") = 15 @@ -383,6 +384,8 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( } sentry_options_set_database_path(options, database_path); sentry_free(database_path); + sentry_free(outbox_path); + outbox_path = NULL; dsn_str = call_get_string(env, sentry_ndk_options, dsn_mid); ENSURE_OR_FAIL(dsn_str); @@ -423,12 +426,7 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( return (jint) rv; fail: - if (!transport_owns_path) { - sentry_free(outbox_path); - } - if (!options_owns_transport) { - sentry_transport_free(transport); - } + sentry_free(outbox_path); sentry_options_free(options); return (jint) -1; } diff --git a/ndk/lib/src/main/jni/sentry_android_transport.c b/ndk/lib/src/main/jni/sentry_android_transport.c new file mode 100644 index 0000000000..8cfbec7d6f --- /dev/null +++ b/ndk/lib/src/main/jni/sentry_android_transport.c @@ -0,0 +1,126 @@ +#include "sentry_alloc.h" +#include "sentry_logger.h" +#include "sentry_options.h" +#include "sentry_path.h" +#include "sentry_transport.h" + +#include +#include +#include +#include + +#define ENSURE(Expr) \ + if (!(Expr)) \ + return + +static int +android_transport_startup(const sentry_options_t *options, void *data) +{ + char **outbox_path = data; + if (!outbox_path || !options->run || !options->run->run_path) { + return 1; + } + + sentry_path_t *database_path = sentry__path_dir(options->run->run_path); + if (!database_path) { + return 1; + } + sentry_path_t *files_dir = sentry__path_dir(database_path); + sentry__path_free(database_path); + if (!files_dir) { + return 1; + } + + sentry_path_t *outbox_dir = sentry__path_join_str(files_dir, "outbox"); + sentry__path_free(files_dir); + if (!outbox_dir) { + return 1; + } + + if (sentry__path_create_dir_all(outbox_dir) != 0) { + SENTRY_WARNF("android transport startup: mkdir failed for %s: %s", + outbox_dir->path, strerror(errno)); + sentry__path_free(outbox_dir); + return 1; + } + + char *outbox_path_str = sentry_malloc(strlen(outbox_dir->path) + 1); + if (!outbox_path_str) { + sentry__path_free(outbox_dir); + return 1; + } + strcpy(outbox_path_str, outbox_dir->path); + sentry__path_free(outbox_dir); + + sentry_free(*outbox_path); + *outbox_path = outbox_path_str; + return 0; +} + +static void +send_envelope(sentry_envelope_t *envelope, void *data) +{ + const char *outbox_path = (const char *)data; + char envelope_id_str[40]; + + sentry_uuid_t envelope_id = sentry_uuid_new_v4(); + sentry_uuid_as_string(&envelope_id, envelope_id_str); + + size_t outbox_len = strlen(outbox_path); + size_t final_len = outbox_len + 42; // "/" + envelope_id_str + "\0" = 42 + char *envelope_path = sentry_malloc(final_len); + ENSURE(envelope_path); + int written = snprintf( + envelope_path, final_len, "%s/%s", outbox_path, envelope_id_str); + if (written > outbox_len && written < final_len) { + sentry_envelope_write_to_file(envelope, envelope_path); + } + + sentry_free(envelope_path); + sentry_envelope_free(envelope); +} + +static void +android_transport_send_envelope(sentry_envelope_t *envelope, void *data) +{ + char **outbox_path = data; + if (!outbox_path || !*outbox_path) { + SENTRY_WARN( + "android transport send_envelope: outbox path not initialized"); + sentry_envelope_free(envelope); + return; + } + send_envelope(envelope, *outbox_path); +} + +static void +android_transport_free(void *data) +{ + char **outbox_path = data; + if (!outbox_path) { + return; + } + sentry_free(*outbox_path); + sentry_free(outbox_path); +} + +sentry_transport_t * +sentry__transport_new_default(void) +{ + char **outbox_path = sentry__calloc(1, sizeof(*outbox_path)); + if (!outbox_path) { + return NULL; + } + + sentry_transport_t *transport + = sentry_transport_new(android_transport_send_envelope); + if (!transport) { + sentry_free(outbox_path); + return NULL; + } + + sentry_transport_set_state(transport, outbox_path); + sentry_transport_set_free_func(transport, android_transport_free); + sentry_transport_set_startup_func(transport, android_transport_startup); + return transport; +} diff --git a/ndk/sample/CMakeLists.txt b/ndk/sample/CMakeLists.txt index 4cdf8d8dc9..887574a8f1 100644 --- a/ndk/sample/CMakeLists.txt +++ b/ndk/sample/CMakeLists.txt @@ -6,15 +6,15 @@ set(SENTRY_BUILD_SHARED_LIBS ON) add_library(ndk-sample SHARED src/main/cpp/ndk-sample.cpp) -# Adding sentry-native project -add_subdirectory(${SENTRY_NATIVE_SRC} sentry_build) +# Use sentry-native from the NDK library's Prefab package +find_package(sentry-native-ndk REQUIRED CONFIG) # Android logging library find_library(LOG_LIB log) target_link_libraries(ndk-sample PRIVATE ${LOG_LIB} - $ + sentry-native-ndk::sentry ) # Support 16KB page sizes target_link_options(ndk-sample PRIVATE "-Wl,-z,max-page-size=16384") diff --git a/ndk/sample/build.gradle.kts b/ndk/sample/build.gradle.kts index d9bd35d893..2f17573456 100644 --- a/ndk/sample/build.gradle.kts +++ b/ndk/sample/build.gradle.kts @@ -3,12 +3,13 @@ plugins { kotlin("android") } -var sentryNativeSrc: String = "${project.projectDir}/../.." - android { compileSdk = 35 namespace = "io.sentry.ndk.sample" - buildFeatures.buildConfig = true + buildFeatures { + buildConfig = true + prefab = true + } defaultConfig { applicationId = "io.sentry.ndk.sample" @@ -20,7 +21,6 @@ android { externalNativeBuild { cmake { arguments.add(0, "-DANDROID_STL=c++_shared") - arguments.add(0, "-DSENTRY_NATIVE_SRC=$sentryNativeSrc") } } diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index 839a224aac..2a7b9ba1fd 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -26,6 +26,7 @@ # include "sentry_minidump_format.h" # include "sentry_minidump_indirect.h" # include "sentry_minidump_writer.h" +# include "sentry_os.h" // NT_PRSTATUS is defined in linux/elf.h but we can't include that // because it conflicts with elf.h. Define it here if not available. @@ -66,9 +67,6 @@ struct fpsimd_context { # endif # endif -// Use process_vm_readv to read memory from crashed process -# include - // Use shared constants from crash context # include "../sentry_crash_context.h" @@ -346,7 +344,8 @@ read_process_memory( struct iovec local_iov = { .iov_base = buf, .iov_len = len }; struct iovec remote_iov = { .iov_base = (void *)addr, .iov_len = len }; - ssize_t nread = process_vm_readv(tid, &local_iov, 1, &remote_iov, 1, 0); + ssize_t nread + = sentry__process_vm_readv(tid, &local_iov, 1, &remote_iov, 1, 0); if (nread > 0) { return nread; } @@ -2135,7 +2134,7 @@ linux_indirect_read_memory(void *ctx, uint64_t addr, void *buf, size_t len) pid_t tid = writer->crash_ctx->crashed_tid; struct iovec local_iov = { .iov_base = buf, .iov_len = len }; struct iovec remote_iov = { .iov_base = (void *)addr, .iov_len = len }; - return process_vm_readv(tid, &local_iov, 1, &remote_iov, 1, 0); + return sentry__process_vm_readv(tid, &local_iov, 1, &remote_iov, 1, 0); } /** diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 002b060160..1e5b094670 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -10,6 +10,7 @@ #include "sentry_json.h" #include "sentry_logger.h" #include "sentry_options.h" +#include "sentry_os.h" #include "sentry_path.h" #include "sentry_process.h" #include "sentry_screenshot.h" @@ -801,8 +802,8 @@ build_stacktrace_for_thread( = { .iov_base = stack_buf, .iov_len = stack_size }; struct iovec remote_iov = { .iov_base = (void *)stack_start, .iov_len = stack_size }; - ssize_t bytes_read - = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0); + ssize_t bytes_read = sentry__process_vm_readv( + pid, &local_iov, 1, &remote_iov, 1, 0); if (bytes_read <= 0) { SENTRY_DEBUG( "process_vm_readv failed, falling back to single frame"); diff --git a/src/modulefinder/sentry_modulefinder_linux.c b/src/modulefinder/sentry_modulefinder_linux.c index a361dbae16..fd527292d8 100644 --- a/src/modulefinder/sentry_modulefinder_linux.c +++ b/src/modulefinder/sentry_modulefinder_linux.c @@ -4,6 +4,7 @@ #include "sentry_modulefinder_linux.h" #include "sentry_core.h" +#include "sentry_os.h" #include "sentry_path.h" #include "sentry_string.h" #include "sentry_sync.h" @@ -15,22 +16,10 @@ #include #include #include -#include #include #include #include -#if defined(__ANDROID_API__) && __ANDROID_API__ < 23 -static ssize_t -process_vm_readv(pid_t __pid, const struct iovec *__local_iov, - unsigned long __local_iov_count, const struct iovec *__remote_iov, - unsigned long __remote_iov_count, unsigned long __flags) -{ - return syscall(__NR_process_vm_readv, __pid, __local_iov, __local_iov_count, - __remote_iov, __remote_iov_count, __flags); -} -#endif - #define ENSURE(Ptr) \ if (!Ptr) \ goto fail @@ -132,7 +121,7 @@ read_safely(void *dst, void *src, size_t size) remote[0].iov_len = size; errno = 0; - ssize_t nread = process_vm_readv(pid, local, 1, remote, 1, 0); + ssize_t nread = sentry__process_vm_readv(pid, local, 1, remote, 1, 0); bool rv = nread == (ssize_t)size; // The syscall can fail with `EPERM` if we lack permissions for this syscall diff --git a/src/sentry_os.c b/src/sentry_os.c index e1bb18cb94..f7f7a58d73 100644 --- a/src/sentry_os.c +++ b/src/sentry_os.c @@ -7,9 +7,28 @@ # include "sentry_utils.h" #endif #ifdef SENTRY_PLATFORM_LINUX +# include +# include # include #endif +#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +ssize_t +sentry__process_vm_readv(pid_t pid, const struct iovec *local_iov, + unsigned long local_iov_count, const struct iovec *remote_iov, + unsigned long remote_iov_count, unsigned long flags) +{ +# if defined(SENTRY_PLATFORM_ANDROID) && defined(__ANDROID_API__) \ + && __ANDROID_API__ < 23 + return syscall(__NR_process_vm_readv, pid, local_iov, local_iov_count, + remote_iov, remote_iov_count, flags); +# else + return process_vm_readv( + pid, local_iov, local_iov_count, remote_iov, remote_iov_count, flags); +# endif +} +#endif + #ifdef SENTRY_PLATFORM_WINDOWS # include diff --git a/src/sentry_os.h b/src/sentry_os.h index 05ed866b6c..c9d77d9fe6 100644 --- a/src/sentry_os.h +++ b/src/sentry_os.h @@ -3,6 +3,11 @@ #include "sentry_boot.h" +#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +# include +# include +#endif + #ifdef SENTRY_PLATFORM_WINDOWS typedef struct { @@ -25,6 +30,12 @@ void sentry__win32_restore_sigabrt_handler(void); #endif +#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +ssize_t sentry__process_vm_readv(pid_t pid, const struct iovec *local_iov, + unsigned long local_iov_count, const struct iovec *remote_iov, + unsigned long remote_iov_count, unsigned long flags); +#endif + sentry_value_t sentry__get_os_context(void); #endif From 11f94c3b0c8a454b374ce1286180b1f09fbed352 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 19 May 2026 15:33:13 +0200 Subject: [PATCH 11/22] curl --- ndk/gradle.properties | 3 +++ ndk/lib/CMakeLists.txt | 9 ++++++++- ndk/lib/build.gradle.kts | 4 ++++ ndk/sample/src/main/AndroidManifest.xml | 2 ++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ndk/gradle.properties b/ndk/gradle.properties index 59b2e28fce..fbd23e9dc9 100644 --- a/ndk/gradle.properties +++ b/ndk/gradle.properties @@ -50,3 +50,6 @@ systemProp.org.gradle.internal.http.socketTimeout=120000 android.nonTransitiveRClass=true android.suppressUnsupportedCompileSdk=34 + +# TODO: clean up +sentryBackend=native diff --git a/ndk/lib/CMakeLists.txt b/ndk/lib/CMakeLists.txt index e7bf8e4017..dbf8d05956 100644 --- a/ndk/lib/CMakeLists.txt +++ b/ndk/lib/CMakeLists.txt @@ -1,6 +1,10 @@ cmake_minimum_required(VERSION 3.10) project(Sentry-Android LANGUAGES C CXX) +if(POLICY CMP0079) + cmake_policy(SET CMP0079 NEW) +endif() + # Add sentry-android shared library add_library(sentry-android SHARED src/main/jni/sentry.c) @@ -21,8 +25,10 @@ target_sources(sentry PRIVATE ) if(SENTRY_BACKEND STREQUAL "native") + find_package(curl REQUIRED CONFIG) + # TODO: SENTRY_TRANSPORT=curl target_sources(sentry-crash PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jni/sentry_android_transport.c + ${SENTRY_NATIVE_SRC}/src/transports/sentry_http_transport_curl.c ) target_compile_definitions(sentry-android PRIVATE SENTRY_BACKEND_NATIVE) set_target_properties(sentry-crash PROPERTIES @@ -31,6 +37,7 @@ if(SENTRY_BACKEND STREQUAL "native") SUFFIX ".so" ) target_link_options(sentry-crash PRIVATE "-Wl,-z,max-page-size=16384") + target_link_libraries(sentry-crash PRIVATE curl::curl_static) target_link_libraries(sentry-android PRIVATE dl) endif() diff --git a/ndk/lib/build.gradle.kts b/ndk/lib/build.gradle.kts index 91a95f1e4a..cf5ac85866 100644 --- a/ndk/lib/build.gradle.kts +++ b/ndk/lib/build.gradle.kts @@ -53,6 +53,7 @@ android { } buildFeatures { + prefab = true prefabPublishing = true buildConfig = true } @@ -102,6 +103,9 @@ android { } dependencies { + // TODO: this was the first match on maven central.. + implementation("io.github.vvb2060.ndk:curl:8.18.0") + compileOnly("org.jetbrains:annotations:23.0.0") testImplementation("androidx.test.ext:junit:1.3.0") diff --git a/ndk/sample/src/main/AndroidManifest.xml b/ndk/sample/src/main/AndroidManifest.xml index 882d9c1e80..b7eab55e03 100644 --- a/ndk/sample/src/main/AndroidManifest.xml +++ b/ndk/sample/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + Date: Tue, 19 May 2026 15:42:07 +0200 Subject: [PATCH 12/22] clean up --- ndk/lib/CMakeLists.txt | 23 ++-- ndk/lib/src/main/jni/sentry.c | 46 ++++++- .../src/main/jni/sentry_android_transport.c | 126 ------------------ 3 files changed, 52 insertions(+), 143 deletions(-) delete mode 100644 ndk/lib/src/main/jni/sentry_android_transport.c diff --git a/ndk/lib/CMakeLists.txt b/ndk/lib/CMakeLists.txt index dbf8d05956..582d42923b 100644 --- a/ndk/lib/CMakeLists.txt +++ b/ndk/lib/CMakeLists.txt @@ -1,10 +1,6 @@ cmake_minimum_required(VERSION 3.10) project(Sentry-Android LANGUAGES C CXX) -if(POLICY CMP0079) - cmake_policy(SET CMP0079 NEW) -endif() - # Add sentry-android shared library add_library(sentry-android SHARED src/main/jni/sentry.c) @@ -16,20 +12,20 @@ endif() # make sure that we build it as a shared lib instead of a static lib set(BUILD_SHARED_LIBS ON) set(SENTRY_BUILD_SHARED_LIBS ON) -set(SENTRY_TRANSPORT "custom" CACHE STRING "" FORCE) + +if(SENTRY_BACKEND STREQUAL "native") + find_package(curl REQUIRED CONFIG) + if(NOT TARGET CURL::libcurl) + add_library(CURL::libcurl INTERFACE IMPORTED) + target_link_libraries(CURL::libcurl INTERFACE curl::curl_static) + endif() + set(SENTRY_TRANSPORT "curl" CACHE STRING "" FORCE) +endif() # Adding sentry-native project add_subdirectory(${SENTRY_NATIVE_SRC} sentry_build) -target_sources(sentry PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src/main/jni/sentry_android_transport.c -) if(SENTRY_BACKEND STREQUAL "native") - find_package(curl REQUIRED CONFIG) - # TODO: SENTRY_TRANSPORT=curl - target_sources(sentry-crash PRIVATE - ${SENTRY_NATIVE_SRC}/src/transports/sentry_http_transport_curl.c - ) target_compile_definitions(sentry-android PRIVATE SENTRY_BACKEND_NATIVE) set_target_properties(sentry-crash PROPERTIES PREFIX "lib" @@ -37,7 +33,6 @@ if(SENTRY_BACKEND STREQUAL "native") SUFFIX ".so" ) target_link_options(sentry-crash PRIVATE "-Wl,-z,max-page-size=16384") - target_link_libraries(sentry-crash PRIVATE curl::curl_static) target_link_libraries(sentry-android PRIVATE dl) endif() diff --git a/ndk/lib/src/main/jni/sentry.c b/ndk/lib/src/main/jni/sentry.c index 060ee66df8..a0d79cbbea 100644 --- a/ndk/lib/src/main/jni/sentry.c +++ b/ndk/lib/src/main/jni/sentry.c @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -12,6 +13,10 @@ #endif #endif +#define ENSURE(Expr) \ + if (!(Expr)) \ + return + #define ENSURE_OR_FAIL(Expr) \ if (!(Expr)) \ goto fail @@ -311,6 +316,26 @@ Java_io_sentry_ndk_NativeScope_nativeClearAttachments(JNIEnv *env, jclass cls) { sentry_clear_attachments(); } +static void send_envelope(sentry_envelope_t *envelope, void *data) { + const char *outbox_path = (const char *) data; + char envelope_id_str[40]; + + sentry_uuid_t envelope_id = sentry_uuid_new_v4(); + sentry_uuid_as_string(&envelope_id, envelope_id_str); + + size_t outbox_len = strlen(outbox_path); + size_t final_len = outbox_len + 42; // "/" + envelope_id_str + "\0" = 42 + char *envelope_path = sentry_malloc(final_len); + ENSURE(envelope_path); + int written = snprintf(envelope_path, final_len, "%s/%s", outbox_path, envelope_id_str); + if (written > outbox_len && written < final_len) { + sentry_envelope_write_to_file(envelope, envelope_path); + } + + sentry_free(envelope_path); + sentry_envelope_free(envelope); +} + // sentry_backend.h extern void sentry__backend_preload(void); @@ -346,7 +371,10 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( (*env)->DeleteLocalRef(env, options_cls); char *outbox_path = NULL; + sentry_transport_t *transport = NULL; + bool transport_owns_path = false; sentry_options_t *options = NULL; + bool options_owns_transport = false; char *dsn_str = NULL; char *release_str = NULL; char *environment_str = NULL; @@ -372,6 +400,15 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( outbox_path = call_get_string(env, sentry_ndk_options, outbox_path_mid); ENSURE_OR_FAIL(outbox_path); + transport = sentry_transport_new(send_envelope); + ENSURE_OR_FAIL(transport); + sentry_transport_set_state(transport, outbox_path); + sentry_transport_set_free_func(transport, sentry_free); + transport_owns_path = true; + + sentry_options_set_transport(options, transport); + options_owns_transport = true; + // give sentry-native its own database path it can work with, next to the outbox size_t outbox_len = strlen(outbox_path); size_t final_len = outbox_len + 15; // len(".sentry-native\0") = 15 @@ -384,8 +421,6 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( } sentry_options_set_database_path(options, database_path); sentry_free(database_path); - sentry_free(outbox_path); - outbox_path = NULL; dsn_str = call_get_string(env, sentry_ndk_options, dsn_mid); ENSURE_OR_FAIL(dsn_str); @@ -426,7 +461,12 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( return (jint) rv; fail: - sentry_free(outbox_path); + if (!transport_owns_path) { + sentry_free(outbox_path); + } + if (!options_owns_transport) { + sentry_transport_free(transport); + } sentry_options_free(options); return (jint) -1; } diff --git a/ndk/lib/src/main/jni/sentry_android_transport.c b/ndk/lib/src/main/jni/sentry_android_transport.c deleted file mode 100644 index 8cfbec7d6f..0000000000 --- a/ndk/lib/src/main/jni/sentry_android_transport.c +++ /dev/null @@ -1,126 +0,0 @@ -#include "sentry_alloc.h" -#include "sentry_logger.h" -#include "sentry_options.h" -#include "sentry_path.h" -#include "sentry_transport.h" - -#include -#include -#include -#include - -#define ENSURE(Expr) \ - if (!(Expr)) \ - return - -static int -android_transport_startup(const sentry_options_t *options, void *data) -{ - char **outbox_path = data; - if (!outbox_path || !options->run || !options->run->run_path) { - return 1; - } - - sentry_path_t *database_path = sentry__path_dir(options->run->run_path); - if (!database_path) { - return 1; - } - sentry_path_t *files_dir = sentry__path_dir(database_path); - sentry__path_free(database_path); - if (!files_dir) { - return 1; - } - - sentry_path_t *outbox_dir = sentry__path_join_str(files_dir, "outbox"); - sentry__path_free(files_dir); - if (!outbox_dir) { - return 1; - } - - if (sentry__path_create_dir_all(outbox_dir) != 0) { - SENTRY_WARNF("android transport startup: mkdir failed for %s: %s", - outbox_dir->path, strerror(errno)); - sentry__path_free(outbox_dir); - return 1; - } - - char *outbox_path_str = sentry_malloc(strlen(outbox_dir->path) + 1); - if (!outbox_path_str) { - sentry__path_free(outbox_dir); - return 1; - } - strcpy(outbox_path_str, outbox_dir->path); - sentry__path_free(outbox_dir); - - sentry_free(*outbox_path); - *outbox_path = outbox_path_str; - return 0; -} - -static void -send_envelope(sentry_envelope_t *envelope, void *data) -{ - const char *outbox_path = (const char *)data; - char envelope_id_str[40]; - - sentry_uuid_t envelope_id = sentry_uuid_new_v4(); - sentry_uuid_as_string(&envelope_id, envelope_id_str); - - size_t outbox_len = strlen(outbox_path); - size_t final_len = outbox_len + 42; // "/" + envelope_id_str + "\0" = 42 - char *envelope_path = sentry_malloc(final_len); - ENSURE(envelope_path); - int written = snprintf( - envelope_path, final_len, "%s/%s", outbox_path, envelope_id_str); - if (written > outbox_len && written < final_len) { - sentry_envelope_write_to_file(envelope, envelope_path); - } - - sentry_free(envelope_path); - sentry_envelope_free(envelope); -} - -static void -android_transport_send_envelope(sentry_envelope_t *envelope, void *data) -{ - char **outbox_path = data; - if (!outbox_path || !*outbox_path) { - SENTRY_WARN( - "android transport send_envelope: outbox path not initialized"); - sentry_envelope_free(envelope); - return; - } - send_envelope(envelope, *outbox_path); -} - -static void -android_transport_free(void *data) -{ - char **outbox_path = data; - if (!outbox_path) { - return; - } - sentry_free(*outbox_path); - sentry_free(outbox_path); -} - -sentry_transport_t * -sentry__transport_new_default(void) -{ - char **outbox_path = sentry__calloc(1, sizeof(*outbox_path)); - if (!outbox_path) { - return NULL; - } - - sentry_transport_t *transport - = sentry_transport_new(android_transport_send_envelope); - if (!transport) { - sentry_free(outbox_path); - return NULL; - } - - sentry_transport_set_state(transport, outbox_path); - sentry_transport_set_free_func(transport, android_transport_free); - sentry_transport_set_startup_func(transport, android_transport_startup); - return transport; -} From f823609c31baeb5f0bbb468a1c4908b471d843ac Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 19 May 2026 15:53:40 +0200 Subject: [PATCH 13/22] fix warnings --- src/backends/native/sentry_crash_daemon.c | 1 + src/transports/sentry_http_transport_curl.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 1e5b094670..aa86a5ab1a 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -352,6 +352,7 @@ build_registers_from_ctx(const sentry_crash_context_t *ctx, size_t thread_idx) && thread_idx < ctx->platform.num_threads) { uctx = &ctx->platform.threads[thread_idx].context; } + (void)uctx; # if defined(__x86_64__) uintptr_t *mctx = (uintptr_t *)&uctx->uc_mcontext; diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 5eaee4e1ce..2e49341e8a 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -225,7 +225,7 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, CURL *curl = client->curl_handle; curl_easy_reset(curl); if (client->debug) { - curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); curl_easy_setopt(curl, CURLOPT_WRITEDATA, stderr); // CURLOPT_WRITEFUNCTION will `fwrite` by default } else { From 9ffa097bb0ef382fdbab6dd20b16a8bae6d7e7b2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 19 May 2026 20:24:29 +0200 Subject: [PATCH 14/22] fix(android): Unwind native daemon crashes with libunwindstack Use libunwindstack for Android native-backend crash events so daemon-captured crashes get the same stack frames as in-process events. Filter /proc maps to real ELF images and keep separate mappings by load base to avoid bogus debug images from named non-ELF mappings. Co-Authored-By: OpenAI Codex --- src/CMakeLists.txt | 6 ++ src/backends/native/sentry_crash_daemon.c | 85 +++++++++++++++++-- src/backends/native/sentry_crash_unwind.h | 25 ++++++ .../sentry_crash_unwind_libunwindstack.cpp | 69 +++++++++++++++ 4 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 src/backends/native/sentry_crash_unwind.h create mode 100644 src/backends/native/sentry_crash_unwind_libunwindstack.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 97baa46bfb..d4ed4ca926 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -167,9 +167,15 @@ elseif(SENTRY_BACKEND_NATIVE) backends/native/sentry_crash_ipc.c backends/native/sentry_crash_daemon.c backends/native/sentry_crash_handler.c + backends/native/sentry_crash_unwind.h backends/native/minidump/sentry_minidump_format.h backends/native/minidump/sentry_minidump_writer.h ) + if(SENTRY_WITH_LIBUNWINDSTACK) + sentry_target_sources_cwd(sentry + backends/native/sentry_crash_unwind_libunwindstack.cpp + ) + endif() # Platform-specific minidump writers if(LINUX OR ANDROID) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index aa86a5ab1a..08ecbcf2f5 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -5,6 +5,7 @@ #include "sentry_attachment.h" #include "sentry_core.h" #include "sentry_crash_ipc.h" +#include "sentry_crash_unwind.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_json.h" @@ -792,6 +793,47 @@ build_stacktrace_for_thread( } } #elif defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +# if defined(SENTRY_PLATFORM_ANDROID) \ + && defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK) + if (thread_idx == SIZE_MAX || thread_idx == 0) { + uint64_t unwind_ips[MAX_STACK_FRAMES]; + size_t unwind_count = sentry__crash_unwind_stack_libunwindstack( + ctx->crashed_pid, thread_context, unwind_ips, MAX_STACK_FRAMES); + if (unwind_count > 0) { + sentry_value_t temp_frames[MAX_STACK_FRAMES]; + int frame_count = 0; + for (size_t i = 0; + i < unwind_count && frame_count < MAX_STACK_FRAMES; i++) { + uint64_t frame_ip = unwind_ips[i]; + if (frame_ip == 0 || !is_valid_code_addr(frame_ip)) { + continue; + } + temp_frames[frame_count] = sentry_value_new_object(); + sentry_value_set_by_key(temp_frames[frame_count], + "instruction_addr", sentry__value_new_addr(frame_ip)); + sentry_value_set_by_key(temp_frames[frame_count], "trust", + sentry_value_new_string(i == 0 ? "context" : "cfi")); + enrich_frame_with_module_info( + ctx, temp_frames[frame_count], frame_ip); + frame_count++; + } + + if (frame_count > 0) { + for (int i = frame_count - 1; i >= 0; i--) { + sentry_value_append(frames, temp_frames[i]); + } + sentry_value_set_by_key(stacktrace, "frames", frames); + sentry_value_set_by_key(stacktrace, "registers", + build_registers_from_ctx(ctx, thread_idx)); + sentry_value_set_by_key(stacktrace, + "instruction_addr_adjustment", + sentry_value_new_string("none")); + return stacktrace; + } + } + } +# endif + // On Linux, use process_vm_readv to read stack memory from crashed process if (ctx->platform.num_threads > 0) { pid_t pid = ctx->crashed_pid; @@ -1070,6 +1112,24 @@ build_stacktrace_for_thread( #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) # include +/** + * Quickly verify a file is an ELF binary by reading its magic bytes. + */ +static bool +is_elf_file(const char *path) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) { + return false; + } + + unsigned char magic[SELFMAG]; + bool is_elf = read(fd, magic, SELFMAG) == (ssize_t)SELFMAG + && memcmp(magic, ELFMAG, SELFMAG) == 0; + close(fd); + return is_elf; +} + /** * Extract Build ID from ELF file (for debug_meta) * Returns the Build ID length, or 0 if not found @@ -1226,6 +1286,13 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) continue; // No pathname or special mapping like [stack] } + // Only readable real ELF files are useful as debug images. This skips + // device files, ashmem/memfd regions, fonts, APK resources, and other + // named mappings that can otherwise create bogus image ranges. + if (perms[0] != 'r') { + continue; + } + const char *pathname = line + pathname_offset; // Trim newline size_t len = strlen(pathname); @@ -1235,12 +1302,23 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) if (len == 0) { continue; } + ((char *)pathname)[len] = '\0'; + + if (offset > start || !is_elf_file(pathname)) { + continue; + } + + // Calculate base address: for PIE binaries, the file offset tells us + // how far into the file this mapping starts, so we subtract it to get + // the actual load base address. + uint64_t base_address = start - offset; // Check if this file is already captured - if so, extend size if needed sentry_module_info_t *existing_mod = NULL; for (uint32_t j = 0; j < ctx->module_count; j++) { if (strncmp(ctx->modules[j].name, pathname, len) == 0 - && ctx->modules[j].name[len] == '\0') { + && ctx->modules[j].name[len] == '\0' + && ctx->modules[j].base_address == base_address) { existing_mod = &ctx->modules[j]; break; } @@ -1261,10 +1339,7 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) sentry_module_info_t *mod = &ctx->modules[ctx->module_count]; - // Calculate base address: for PIE binaries, the file offset tells us - // how far into the file this mapping starts, so we subtract it to get - // the actual load base address - mod->base_address = start - offset; + mod->base_address = base_address; // Initial size covers from base to end of this mapping mod->size = end - mod->base_address; diff --git a/src/backends/native/sentry_crash_unwind.h b/src/backends/native/sentry_crash_unwind.h new file mode 100644 index 0000000000..62eb2c32d2 --- /dev/null +++ b/src/backends/native/sentry_crash_unwind.h @@ -0,0 +1,25 @@ +#ifndef SENTRY_CRASH_UNWIND_H_INCLUDED +#define SENTRY_CRASH_UNWIND_H_INCLUDED + +#include "sentry_boot.h" + +#if defined(SENTRY_PLATFORM_ANDROID) \ + && defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK) +# include +# include +# include +# include + +# ifdef __cplusplus +extern "C" { +# endif + +size_t sentry__crash_unwind_stack_libunwindstack( + pid_t pid, const ucontext_t *uctx, uint64_t *ips, size_t max_frames); + +# ifdef __cplusplus +} +# endif +#endif + +#endif diff --git a/src/backends/native/sentry_crash_unwind_libunwindstack.cpp b/src/backends/native/sentry_crash_unwind_libunwindstack.cpp new file mode 100644 index 0000000000..d4ceb7b7b0 --- /dev/null +++ b/src/backends/native/sentry_crash_unwind_libunwindstack.cpp @@ -0,0 +1,69 @@ +#include "sentry_crash_unwind.h" + +#if defined(SENTRY_PLATFORM_ANDROID) \ + && defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK) + +extern "C" { +# include "sentry_logger.h" +} + +# include +# include +# include +# include +# include + +extern "C" size_t +sentry__crash_unwind_stack_libunwindstack( + pid_t pid, const ucontext_t *uctx, uint64_t *ips, size_t max_frames) +{ + if (pid <= 0 || !uctx || !ips || max_frames == 0) { + return 0; + } + + std::unique_ptr regs( + unwindstack::Regs::CreateFromUcontext( + unwindstack::Regs::CurrentArch(), const_cast(uctx))); + if (!regs) { + SENTRY_WARN("libunwindstack failed to create registers from ucontext"); + return 0; + } + + unwindstack::RemoteMaps maps(pid); + if (!maps.Parse()) { + SENTRY_WARNF("libunwindstack failed to parse /proc/%d/maps", pid); + return 0; + } + + std::shared_ptr process_memory + = unwindstack::Memory::CreateProcessMemoryCached(pid); + if (!process_memory) { + SENTRY_WARNF( + "libunwindstack failed to read process memory for %d", pid); + return 0; + } + + unwindstack::Unwinder unwinder( + max_frames, &maps, regs.get(), process_memory); + unwinder.SetResolveNames(false); + unwinder.Unwind(); + + if (unwinder.LastErrorCode() != unwindstack::ERROR_NONE) { + SENTRY_DEBUGF("libunwindstack finished with %s at 0x%llx", + unwinder.LastErrorCodeString(), + (unsigned long long)unwinder.LastErrorAddress()); + } + + size_t count = 0; + for (const unwindstack::FrameData &frame : unwinder.frames()) { + if (count >= max_frames) { + break; + } + ips[count++] = frame.pc; + } + + SENTRY_DEBUGF("libunwindstack remote unwind captured %zu frames", count); + return count; +} + +#endif From bbe88170eb45597fbb0c006c235dd18327513fd1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 19 May 2026 22:15:20 +0200 Subject: [PATCH 15/22] fix(android): Preserve previous native signal handlers Forward handled crash signals to the previously installed signal handler after the native daemon has captured the crash. This lets Android record the process exit as a native app crash and produce a tombstone. Co-Authored-By: OpenAI Codex --- src/backends/native/sentry_crash_handler.c | 48 +++++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index d39a6936c9..82f0b69a01 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -100,6 +100,44 @@ static sentry_crash_ipc_t *g_crash_ipc = NULL; static struct sigaction g_previous_handlers[16]; static stack_t g_signal_stack = { 0 }; +static void +invoke_previous_signal_handler(int signum, siginfo_t *info, void *context) +{ + for (size_t i = 0; i < g_crash_signal_count; i++) { + if (g_crash_signals[i] != signum) { + continue; + } + + struct sigaction *handler = &g_previous_handlers[i]; + // Re-enable previous signal handler to prevent loops. + sigaction(signum, handler, NULL); + + if (handler->sa_handler == SIG_DFL) { + raise(signum); + return; + } + + if (handler->sa_handler == SIG_IGN) { + signal(signum, SIG_DFL); + raise(signum); + return; + } + + if (handler->sa_flags & SA_SIGINFO) { + handler->sa_sigaction(signum, info, context); + } else { + handler->sa_handler(signum); + } + + signal(signum, SIG_DFL); + raise(signum); + return; + } + + signal(signum, SIG_DFL); + raise(signum); +} + /** * Get current thread ID (signal-safe) */ @@ -258,13 +296,10 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) _exit(1); } - // Re-enable signal to prevent loops - signal(signum, SIG_DFL); - sentry_crash_ipc_t *ipc = g_crash_ipc; if (!ipc || !ipc->shmem) { - // No IPC available, just re-raise - raise(signum); + // No IPC available, just re-raise through the previous handler. + invoke_previous_signal_handler(signum, info, context); return; } @@ -763,7 +798,8 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) } } - raise(signum); + // Re-raise through the previous handler after daemon processing. + invoke_previous_signal_handler(signum, info, context); } int From 1251b9a110dc76636c99764a53ca6bae10709d52 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 19 May 2026 22:15:26 +0200 Subject: [PATCH 16/22] fix(android): Queue native crashes for tombstone merging Enable the native daemon to write crash envelopes into the cache directory so the Java SDK can merge tombstones before sending. This keeps native backend crashes on the Android tombstone path instead of sending immediately from the daemon. Co-Authored-By: OpenAI Codex --- src/backends/native/sentry_crash_daemon.c | 47 +++++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 08ecbcf2f5..c4d779a04e 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -3169,8 +3169,12 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) add_attachment_refs(envelope, options, run_folder); - bool has_attachment_refs = sentry__envelope_has_content_type( +#if defined(SENTRY_PLATFORM_ANDROID) + bool write_envelope = true; +#else + bool write_envelope = sentry__envelope_has_content_type( envelope, SENTRY_ATTACHMENT_REF_MIME); +#endif SENTRY_DEBUG("Envelope loaded, capturing"); @@ -3181,19 +3185,28 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) // Capture directly, or pass to external crash reporter if (!sentry__launch_external_crash_reporter(options, envelope)) { - if (has_attachment_refs && options && options->run) { - if (!sentry__run_write_envelope(options->run, envelope)) { - SENTRY_WARN( - "Failed to dump crash envelope for resend on restart"); + if (write_envelope) { + if (!options || !options->run + || !sentry__run_write_envelope(options->run, envelope)) { + SENTRY_WARN("Failed to dump crash envelope to run folder"); + } else { +#if defined(SENTRY_PLATFORM_ANDROID) + SENTRY_DEBUG( + "Crash envelope written for Android tombstone merging"); + sentry_envelope_free(envelope); + envelope = NULL; +#endif } } - if (options && options->transport && options->run) { - SENTRY_DEBUG("Capturing crash envelope"); - sentry__capture_envelope(options->transport, envelope, options); - SENTRY_DEBUG("Crash envelope captured (queued)"); - } else { - SENTRY_WARN("No transport available for sending envelope"); - sentry_envelope_free(envelope); + if (envelope) { + if (options && options->transport && options->run) { + SENTRY_DEBUG("Capturing crash envelope"); + sentry__capture_envelope(options->transport, envelope, options); + SENTRY_DEBUG("Crash envelope captured (queued)"); + } else { + SENTRY_WARN("No transport available for sending envelope"); + sentry_envelope_free(envelope); + } } } @@ -3593,7 +3606,17 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, dumped_envelopes = sentry__transport_dump_queue( options->transport, options->run); if (rv == 0 && !dumped_envelopes && options->run) { +#if defined(SENTRY_PLATFORM_ANDROID) + if (!crash_processed) { + sentry__run_clean(options->run, true); + } else { + SENTRY_DEBUG( + "Keeping daemon run folder for Android tombstone " + "merging"); + } +#else sentry__run_clean(options->run, true); +#endif } } sentry_options_free(options); From 71655dcc60d83811e9ed37a842884d55b7d02cf8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 19 May 2026 22:15:32 +0200 Subject: [PATCH 17/22] fix(android): Use native crash reporting without minidumps Keep the Android native backend enabled while disabling minidump capture for the sample app. This exercises tombstone merging for native backend crashes without sending Breakpad minidumps. Co-Authored-By: OpenAI Codex --- ndk/lib/src/main/jni/sentry.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ndk/lib/src/main/jni/sentry.c b/ndk/lib/src/main/jni/sentry.c index a0d79cbbea..040b8e8f11 100644 --- a/ndk/lib/src/main/jni/sentry.c +++ b/ndk/lib/src/main/jni/sentry.c @@ -386,6 +386,8 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( #if defined(SENTRY_BACKEND_NATIVE) set_handler_path(options); + sentry_options_set_crash_reporting_mode( + options, SENTRY_CRASH_REPORTING_MODE_NATIVE); #endif // session tracking is enabled by default, but the Android SDK already handles it From 1ecaa52c1620f16c4baa0eae3686c3f722d9b4ec Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 20 May 2026 09:56:44 +0200 Subject: [PATCH 18/22] feat(android): Add native backend preload support Install native signal handlers during Android preload without activating crash IPC until sentry_init. This keeps CoreCLR ahead in the signal chain for managed exceptions while native crashes still chain back to sentry-native. Resolve the Android crash daemon path in the native backend so direct C API clients do not need NDK wrapper-specific handler-path setup. Co-Authored-By: OpenAI Codex --- ndk/lib/CMakeLists.txt | 2 - ndk/lib/src/main/jni/sentry.c | 40 --------------- src/backends/native/sentry_crash_handler.c | 60 ++++++++++++++++++---- src/backends/native/sentry_crash_handler.h | 5 ++ src/backends/sentry_backend_native.c | 59 ++++++++++++++++++++- 5 files changed, 111 insertions(+), 55 deletions(-) diff --git a/ndk/lib/CMakeLists.txt b/ndk/lib/CMakeLists.txt index 582d42923b..e7df610586 100644 --- a/ndk/lib/CMakeLists.txt +++ b/ndk/lib/CMakeLists.txt @@ -26,14 +26,12 @@ endif() add_subdirectory(${SENTRY_NATIVE_SRC} sentry_build) if(SENTRY_BACKEND STREQUAL "native") - target_compile_definitions(sentry-android PRIVATE SENTRY_BACKEND_NATIVE) set_target_properties(sentry-crash PROPERTIES PREFIX "lib" OUTPUT_NAME "sentry-crash" SUFFIX ".so" ) target_link_options(sentry-crash PRIVATE "-Wl,-z,max-page-size=16384") - target_link_libraries(sentry-android PRIVATE dl) endif() # Android logging library diff --git a/ndk/lib/src/main/jni/sentry.c b/ndk/lib/src/main/jni/sentry.c index 040b8e8f11..c64f005655 100644 --- a/ndk/lib/src/main/jni/sentry.c +++ b/ndk/lib/src/main/jni/sentry.c @@ -4,15 +4,6 @@ #include #include -#if defined(SENTRY_BACKEND_NATIVE) -#include -#include - -#ifndef PATH_MAX -#define PATH_MAX 4096 -#endif -#endif - #define ENSURE(Expr) \ if (!(Expr)) \ return @@ -68,31 +59,6 @@ static char *call_get_string(JNIEnv *env, jobject obj, jmethodID mid) { return NULL; } -#if defined(SENTRY_BACKEND_NATIVE) -static void set_handler_path(sentry_options_t *options) { - Dl_info info; - if (dladdr((void *)set_handler_path, &info) == 0 || !info.dli_fname) { - return; - } - - const char *slash = strrchr(info.dli_fname, '/'); - if (!slash) { - return; - } - - const char daemon_name[] = "libsentry-crash.so"; - size_t dir_len = (size_t)(slash - info.dli_fname + 1); - if (dir_len + sizeof(daemon_name) > PATH_MAX) { - return; - } - - char handler_path[PATH_MAX]; - memcpy(handler_path, info.dli_fname, dir_len); - memcpy(handler_path + dir_len, daemon_name, sizeof(daemon_name)); - sentry_options_set_handler_path(options, handler_path); -} -#endif - JNIEXPORT void JNICALL Java_io_sentry_ndk_NativeScope_nativeSetTag( JNIEnv *env, @@ -384,12 +350,6 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( options = sentry_options_new(); ENSURE_OR_FAIL(options); -#if defined(SENTRY_BACKEND_NATIVE) - set_handler_path(options); - sentry_options_set_crash_reporting_mode( - options, SENTRY_CRASH_REPORTING_MODE_NATIVE); -#endif - // session tracking is enabled by default, but the Android SDK already handles it sentry_options_set_auto_session_tracking(options, 0); diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index 82f0b69a01..bb77f9e9c9 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -99,6 +99,8 @@ static const size_t g_crash_signal_count static sentry_crash_ipc_t *g_crash_ipc = NULL; static struct sigaction g_previous_handlers[16]; static stack_t g_signal_stack = { 0 }; +static volatile long g_handlers_installed = 0; +static volatile long g_preloaded = 0; static void invoke_previous_signal_handler(int signum, siginfo_t *info, void *context) @@ -289,6 +291,14 @@ safe_build_stack_path( static void crash_signal_handler(int signum, siginfo_t *info, void *context) { + sentry_crash_ipc_t *ipc = g_crash_ipc; + if (!ipc || !ipc->shmem) { + // Preload only establishes signal-chain order. Until sentry_init() + // activates IPC, fall through to the previous handler. + invoke_previous_signal_handler(signum, info, context); + return; + } + // Only handle crash once - check if already processing static volatile long handling_crash = 0; if (!sentry__atomic_compare_swap(&handling_crash, 0, 1)) { @@ -296,13 +306,6 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) _exit(1); } - sentry_crash_ipc_t *ipc = g_crash_ipc; - if (!ipc || !ipc->shmem) { - // No IPC available, just re-raise through the previous handler. - invoke_previous_signal_handler(signum, info, context); - return; - } - sentry_crash_context_t *ctx = ipc->shmem; ucontext_t *uctx = (ucontext_t *)context; @@ -802,14 +805,19 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) invoke_previous_signal_handler(signum, info, context); } -int -sentry__crash_handler_init(sentry_crash_ipc_t *ipc) +static int +install_signal_handlers(sentry_crash_ipc_t *ipc, bool preload) { - if (!ipc) { + if (!preload && !ipc) { return -1; } - g_crash_ipc = ipc; + if (sentry__atomic_fetch(&g_handlers_installed)) { + if (!preload) { + g_crash_ipc = ipc; + } + return 0; + } // Set up signal stack g_signal_stack.ss_sp = sentry_malloc(SENTRY_CRASH_SIGNAL_STACK_SIZE); @@ -855,6 +863,12 @@ sentry__crash_handler_init(sentry_crash_ipc_t *ipc) installed_count++; } + g_crash_ipc = ipc; + sentry__atomic_store(&g_handlers_installed, 1); + if (preload) { + sentry__atomic_store(&g_preloaded, 1); + } + // Prime libunwind's internal cache by performing a short unwind. // This forces dl_iterate_phdr to be called now (while safe) rather than // in the signal handler where it could deadlock if the crash occurs @@ -888,9 +902,31 @@ sentry__crash_handler_init(sentry_crash_ipc_t *ipc) return 0; } +int +sentry__crash_handler_init(sentry_crash_ipc_t *ipc) +{ + return install_signal_handlers(ipc, false); +} + +int +sentry__crash_handler_preload(void) +{ + return install_signal_handlers(NULL, true); +} + void sentry__crash_handler_shutdown(void) { + if (sentry__atomic_fetch(&g_preloaded)) { + g_crash_ipc = NULL; + SENTRY_DEBUG("crash handler deactivated, keeping preloaded handlers"); + return; + } + + if (!sentry__atomic_fetch(&g_handlers_installed)) { + return; + } + // Restore previous signal handlers for (size_t i = 0; i < g_crash_signal_count; i++) { sigaction(g_crash_signals[i], &g_previous_handlers[i], NULL); @@ -905,6 +941,8 @@ sentry__crash_handler_shutdown(void) } g_crash_ipc = NULL; + sentry__atomic_store(&g_handlers_installed, 0); + sentry__atomic_store(&g_preloaded, 0); SENTRY_DEBUG("crash handler shutdown"); } diff --git a/src/backends/native/sentry_crash_handler.h b/src/backends/native/sentry_crash_handler.h index 943a1df61b..4a3509c2ba 100644 --- a/src/backends/native/sentry_crash_handler.h +++ b/src/backends/native/sentry_crash_handler.h @@ -9,6 +9,11 @@ */ int sentry__crash_handler_init(sentry_crash_ipc_t *ipc); +/** + * Preload crash handler before full initialization. + */ +int sentry__crash_handler_preload(void); + /** * Shutdown crash handler (restore previous handlers) */ diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index e8cd5a8b2f..853c0070e5 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -11,6 +11,9 @@ # if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) # include # endif +# if defined(SENTRY_PLATFORM_ANDROID) +# include +# endif #elif defined(SENTRY_PLATFORM_WINDOWS) && !defined(SENTRY_PLATFORM_XBOX) # include #endif @@ -55,6 +58,53 @@ static sem_t *g_ipc_init_sem = SEM_FAILED; static char g_ipc_sem_name[64] = { 0 }; #endif +static char * +resolve_handler_path(const sentry_options_t *options) +{ + if (options->handler_path) { + return sentry__string_clone(options->handler_path->path); + } + +#if defined(SENTRY_PLATFORM_ANDROID) + Dl_info info; + if (dladdr((void *)resolve_handler_path, &info) == 0 || !info.dli_fname) { + SENTRY_WARN("unable to resolve sentry library path for crash daemon"); + return NULL; + } + + sentry_path_t *library_path = sentry__path_from_str(info.dli_fname); + if (!library_path) { + return NULL; + } + + sentry_path_t *library_dir = sentry__path_dir(library_path); + sentry__path_free(library_path); + if (!library_dir) { + return NULL; + } + + sentry_path_t *handler_path + = sentry__path_join_str(library_dir, "libsentry-crash.so"); + sentry__path_free(library_dir); + if (!handler_path) { + return NULL; + } + + if (!sentry__path_is_file(handler_path)) { + SENTRY_WARNF("Android crash daemon is missing: %s", handler_path->path); + sentry__path_free(handler_path); + return NULL; + } + + SENTRY_DEBUGF("resolved Android crash daemon path: %s", handler_path->path); + char *handler_path_str = sentry__string_clone(handler_path->path); + sentry__path_free(handler_path); + return handler_path_str; +#else + return NULL; +#endif +} + // Mutex to protect IPC initialization (Windows and Linux only, not macOS/iOS or // Android) macOS/Android use g_ipc_sync_mutex directly; iOS has no // out-of-process daemon. @@ -458,8 +508,7 @@ native_backend_startup( #else // Other platforms: Use out-of-process daemon // Pass the notification handles (eventfd/pipe on Unix, events on Windows) - const char *daemon_handler_path - = options->handler_path ? options->handler_path->path : NULL; + char *daemon_handler_path = resolve_handler_path(options); # if defined(SENTRY_PLATFORM_ANDROID) uint64_t tid = (uint64_t)pthread_self(); state->daemon_pid @@ -480,6 +529,7 @@ native_backend_startup( state->ipc->event_handle, state->ipc->ready_event_handle, daemon_handler_path); # endif + sentry_free(daemon_handler_path); // On Windows, pid_t is DWORD (unsigned), so (pid_t)-1 == 0xFFFFFFFF. // On Unix, pid_t is signed and fork returns -1 on failure. @@ -1144,6 +1194,11 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) void sentry__backend_preload(void) { +#ifdef SENTRY_PLATFORM_ANDROID + if (sentry__crash_handler_preload() < 0) { + SENTRY_WARN("failed to preload native crash handler"); + } +#endif } /** From f6b5cafaaf5ee1c86381fa3efc554908a5b8f92f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 20 May 2026 10:16:15 +0200 Subject: [PATCH 19/22] fix(android): Respect tombstone setting in NDK crash mode Pass the Android tombstone setting through NdkOptions to choose the native crash reporting mode. Use native-only mode when tombstone merging is enabled so the native envelope can be merged, and include a minidump when tombstone merging is disabled. Co-Authored-By: OpenAI Codex --- ndk/lib/api/sentry-native-ndk.api | 2 ++ ndk/lib/src/main/java/io/sentry/ndk/NdkOptions.java | 9 +++++++++ ndk/lib/src/main/jni/sentry.c | 10 ++++++++++ 3 files changed, 21 insertions(+) diff --git a/ndk/lib/api/sentry-native-ndk.api b/ndk/lib/api/sentry-native-ndk.api index 9d6c41fc3a..71422c6aa5 100644 --- a/ndk/lib/api/sentry-native-ndk.api +++ b/ndk/lib/api/sentry-native-ndk.api @@ -96,7 +96,9 @@ public final class io/sentry/ndk/NdkOptions { public fun getSdkName ()Ljava/lang/String; public fun getTracesSampleRate ()F public fun isDebug ()Z + public fun isTombstoneEnabled ()Z public fun setNdkHandlerStrategy (Lio/sentry/ndk/NdkHandlerStrategy;)V + public fun setTombstoneEnabled (Z)V public fun setTracesSampleRate (F)V } diff --git a/ndk/lib/src/main/java/io/sentry/ndk/NdkOptions.java b/ndk/lib/src/main/java/io/sentry/ndk/NdkOptions.java index e0cd5e47b3..eb38dbe721 100644 --- a/ndk/lib/src/main/java/io/sentry/ndk/NdkOptions.java +++ b/ndk/lib/src/main/java/io/sentry/ndk/NdkOptions.java @@ -15,6 +15,7 @@ public final class NdkOptions { private NdkHandlerStrategy ndkHandlerStrategy = NdkHandlerStrategy.SENTRY_HANDLER_STRATEGY_DEFAULT; private float tracesSampleRate = 0; + private boolean tombstoneEnabled = false; public NdkOptions( @NotNull String dsn, @@ -88,4 +89,12 @@ public void setTracesSampleRate(final float tracesSampleRate) { public float getTracesSampleRate() { return tracesSampleRate; } + + public void setTombstoneEnabled(final boolean tombstoneEnabled) { + this.tombstoneEnabled = tombstoneEnabled; + } + + public boolean isTombstoneEnabled() { + return tombstoneEnabled; + } } diff --git a/ndk/lib/src/main/jni/sentry.c b/ndk/lib/src/main/jni/sentry.c index c64f005655..0c82bcf4cc 100644 --- a/ndk/lib/src/main/jni/sentry.c +++ b/ndk/lib/src/main/jni/sentry.c @@ -333,6 +333,7 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( jmethodID handler_strategy_mid = (*env)->GetMethodID(env, options_cls, "getNdkHandlerStrategy", "()I"); jmethodID traces_sample_rate_mid = (*env)->GetMethodID(env, options_cls, "getTracesSampleRate", "()F"); + jmethodID tombstone_enabled_mid = (*env)->GetMethodID(env, options_cls, "isTombstoneEnabled", "()Z"); (*env)->DeleteLocalRef(env, options_cls); @@ -350,6 +351,15 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( options = sentry_options_new(); ENSURE_OR_FAIL(options); + // Minidumps take precedence over native envelopes during processing. When + // tombstone merging is enabled, use native-only mode so the native envelope + // can be enriched; otherwise attach a minidump for extra crash context. + jboolean tombstone_enabled = + (jboolean)(*env)->CallBooleanMethod(env, sentry_ndk_options, tombstone_enabled_mid); + sentry_options_set_crash_reporting_mode(options, + tombstone_enabled ? SENTRY_CRASH_REPORTING_MODE_NATIVE + : SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP); + // session tracking is enabled by default, but the Android SDK already handles it sentry_options_set_auto_session_tracking(options, 0); From 0d6923e57a1976192248fbabd0e15d9553afbe77 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 20 May 2026 11:17:16 +0200 Subject: [PATCH 20/22] test(android): Clean native test database after crash Remove the native test database after the Android minidump assertion so later tests do not observe stale crash state on the device. Co-Authored-By: OpenAI Codex --- tests/test_integration_android.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_integration_android.py b/tests/test_integration_android.py index 2ac8f4ebde..73ba272fbd 100644 --- a/tests/test_integration_android.py +++ b/tests/test_integration_android.py @@ -52,11 +52,14 @@ def find_minidumps(timeout=10): adb("shell", f"rm -rf {database_path}", check=True) assert find_minidumps(timeout=0) == [] - run(tmp_path, "sentry_example", ["log", "crash"], expect_failure=True) + try: + run(tmp_path, "sentry_example", ["log", "crash"], expect_failure=True) - minidumps = find_minidumps() - assert minidumps, "native backend should create a minidump on Android" + minidumps = find_minidumps() + assert minidumps, "native backend should create a minidump on Android" - local_minidump = tmp_path / "android.dmp" - adb("pull", minidumps[0], str(local_minidump), check=True, capture_output=True) - assert local_minidump.stat().st_size > 0 + local_minidump = tmp_path / "android.dmp" + adb("pull", minidumps[0], str(local_minidump), check=True, capture_output=True) + assert local_minidump.stat().st_size > 0 + finally: + adb("shell", f"rm -rf {database_path}", check=False) From 57004b7f8357671057b6723484064c5514b06576 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 20 May 2026 11:17:39 +0200 Subject: [PATCH 21/22] feat(transport): Add stdout transport Add a built-in stdout transport so integration tests can exercise the default transport path while still writing envelopes to stdout. The native crash daemon resolves the default transport from the SDK build; it cannot depend on the example installing a process-local transport callback. Using SENTRY_TRANSPORT=custom for this test would also consume the downstream extension point, where embedding SDKs must keep providing their own sentry__transport_new_default implementation. Co-Authored-By: OpenAI Codex --- CMakeLists.txt | 2 ++ src/CMakeLists.txt | 4 ++++ src/transports/sentry_transport_stdout.c | 22 ++++++++++++++++++++++ tests/test_integration_stdout.py | 4 ++-- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/transports/sentry_transport_stdout.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f16ba746b..2440560e34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -187,6 +187,8 @@ elseif(SENTRY_TRANSPORT STREQUAL "curl") set(SENTRY_TRANSPORT_CURL TRUE) elseif(SENTRY_TRANSPORT STREQUAL "none") set(SENTRY_TRANSPORT_NONE TRUE) +elseif(SENTRY_TRANSPORT STREQUAL "stdout") + set(SENTRY_TRANSPORT_STDOUT TRUE) elseif(SENTRY_TRANSPORT STREQUAL "pshttp") # Not implemented here, but in the downstream SDK if(NOT PROSPERO) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d4ed4ca926..21a9a2c5cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -137,6 +137,10 @@ elseif(SENTRY_TRANSPORT_NONE) sentry_target_sources_cwd(sentry transports/sentry_transport_none.c ) +elseif(SENTRY_TRANSPORT_STDOUT) + sentry_target_sources_cwd(sentry + transports/sentry_transport_stdout.c + ) endif() # backends diff --git a/src/transports/sentry_transport_stdout.c b/src/transports/sentry_transport_stdout.c new file mode 100644 index 0000000000..a45f24047d --- /dev/null +++ b/src/transports/sentry_transport_stdout.c @@ -0,0 +1,22 @@ +#include "sentry_core.h" +#include "sentry_transport.h" + +#include + +static void +print_envelope(sentry_envelope_t *envelope, void *unused_state) +{ + (void)unused_state; + size_t size_out = 0; + char *serialized = sentry_envelope_serialize(envelope, &size_out); + printf("%s", serialized); + fflush(stdout); + sentry_free(serialized); + sentry_envelope_free(envelope); +} + +sentry_transport_t * +sentry__transport_new_default(void) +{ + return sentry_transport_new(print_envelope); +} diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index 9d8f66cb30..74d1ade537 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -31,14 +31,14 @@ def test_capture_stdout(cmake): ["sentry_example"], { "SENTRY_BACKEND": "none", - "SENTRY_TRANSPORT": "none", + "SENTRY_TRANSPORT": "stdout", }, ) output = check_output( tmp_path, "sentry_example", - ["stdout", "attachment", "capture-event", "add-stacktrace"], + ["attachment", "capture-event", "add-stacktrace"], ) envelope = Envelope.deserialize(output) From 646020868fc3198e316d00e1061cf69ae72da47d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 20 May 2026 18:07:43 +0200 Subject: [PATCH 22/22] WIP: tombstone merge support --- .../main/java/io/sentry/ndk/SentryNdk.java | 34 ++ ndk/lib/src/main/jni/sentry.c | 101 ++++++ src/backends/native/sentry_crash_daemon.c | 303 +++++++++++++++++- src/backends/native/sentry_crash_daemon.h | 2 +- src/backends/native/sentry_crash_ipc.c | 63 ++-- src/backends/native/sentry_crash_ipc.h | 2 + src/backends/sentry_backend_native.c | 105 +++++- 7 files changed, 573 insertions(+), 37 deletions(-) diff --git a/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java b/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java index 2369c11042..9bbd4357d8 100644 --- a/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java +++ b/ndk/lib/src/main/java/io/sentry/ndk/SentryNdk.java @@ -2,6 +2,7 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @ApiStatus.Internal public final class SentryNdk { @@ -20,6 +21,16 @@ private SentryNdk() {} */ private static native int initSentryNative(@NotNull final NdkOptions options); + private static native void nativeInitCrashDaemon( + @NotNull String shmPath, int notifyFd, int readyFd); + + private static native @Nullable String nativeRunCrashDaemon( + int appPid, long appTid, int notifyFd, int readyFd, @NotNull String shmPath); + + private static native boolean nativeSendEnvelope(@NotNull String path, long timeout); + + private static native void nativeCloseCrashDaemon(); + private static native void shutdown(); /** @@ -57,6 +68,28 @@ public static void init(@NotNull final NdkOptions options) { } } + public static void initCrashDaemon( + @NotNull String shmPath, int notifyFd, int readyFd) { + loadNativeLibraries(); + nativeInitCrashDaemon(shmPath, notifyFd, readyFd); + } + + public static @Nullable String runCrashDaemon( + int appPid, long appTid, int notifyFd, int readyFd, @NotNull String shmPath) { + loadNativeLibraries(); + return nativeRunCrashDaemon(appPid, appTid, notifyFd, readyFd, shmPath); + } + + public static boolean sendEnvelope(@NotNull String path, long timeout) { + loadNativeLibraries(); + return nativeSendEnvelope(path, timeout); + } + + public static void closeCrashDaemon() { + loadNativeLibraries(); + nativeCloseCrashDaemon(); + } + /** Closes the NDK integration */ public static void close() { loadNativeLibraries(); @@ -80,4 +113,5 @@ public static synchronized void loadNativeLibraries() { nativeLibrariesLoaded = true; } } + } diff --git a/ndk/lib/src/main/jni/sentry.c b/ndk/lib/src/main/jni/sentry.c index 0c82bcf4cc..e8e56d6383 100644 --- a/ndk/lib/src/main/jni/sentry.c +++ b/ndk/lib/src/main/jni/sentry.c @@ -1,9 +1,19 @@ #include #include #include +#include #include #include +extern void sentry_android_crash_daemon_init( + const char *shm_path, int notify_fd, int ready_fd) __attribute__((weak)); +extern char *sentry_android_crash_daemon_run(int app_pid, + uint64_t app_tid, int notify_fd, int ready_fd, + const char *shm_path) __attribute__((weak)); +extern bool sentry_android_crash_daemon_send( + const char *path, uint64_t timeout) __attribute__((weak)); +extern void sentry_android_crash_daemon_close(void) __attribute__((weak)); + #define ENSURE(Expr) \ if (!(Expr)) \ return @@ -443,6 +453,97 @@ Java_io_sentry_ndk_SentryNdk_initSentryNative( return (jint) -1; } +JNIEXPORT void JNICALL +Java_io_sentry_ndk_SentryNdk_nativeInitCrashDaemon( + JNIEnv *env, + jclass cls, + jstring shm_path, + jint notify_fd, + jint ready_fd) { + if (!shm_path) { + return; + } + + const char *shm_path_chars = (*env)->GetStringUTFChars(env, shm_path, 0); + if (!shm_path_chars) { + return; + } + + if (sentry_android_crash_daemon_init) { + sentry_android_crash_daemon_init( + shm_path_chars, (int) notify_fd, (int) ready_fd); + } + + (*env)->ReleaseStringUTFChars(env, shm_path, shm_path_chars); +} + +JNIEXPORT jstring JNICALL +Java_io_sentry_ndk_SentryNdk_nativeRunCrashDaemon( + JNIEnv *env, + jclass cls, + jint app_pid, + jlong app_tid, + jint notify_fd, + jint ready_fd, + jstring shm_path) { + if (!shm_path) { + return NULL; + } + + const char *shm_path_chars = (*env)->GetStringUTFChars(env, shm_path, 0); + if (!shm_path_chars) { + return NULL; + } + + char *envelope_path = NULL; + if (sentry_android_crash_daemon_run) { + envelope_path = sentry_android_crash_daemon_run((int) app_pid, + (uint64_t) app_tid, (int) notify_fd, (int) ready_fd, + shm_path_chars); + } + + (*env)->ReleaseStringUTFChars(env, shm_path, shm_path_chars); + if (!envelope_path) { + return NULL; + } + + jstring rv = (*env)->NewStringUTF(env, envelope_path); + sentry_free(envelope_path); + return rv; +} + +JNIEXPORT jboolean JNICALL +Java_io_sentry_ndk_SentryNdk_nativeSendEnvelope( + JNIEnv *env, + jclass cls, + jstring path, + jlong timeout) { + if (!path) { + return JNI_FALSE; + } + + const char *path_chars = (*env)->GetStringUTFChars(env, path, 0); + if (!path_chars) { + return JNI_FALSE; + } + + bool sent = false; + if (sentry_android_crash_daemon_send) { + sent = sentry_android_crash_daemon_send( + path_chars, (uint64_t) timeout); + } + + (*env)->ReleaseStringUTFChars(env, path, path_chars); + return sent ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT void JNICALL +Java_io_sentry_ndk_SentryNdk_nativeCloseCrashDaemon(JNIEnv *env, jclass cls) { + if (sentry_android_crash_daemon_close) { + sentry_android_crash_daemon_close(); + } +} + JNIEXPORT void JNICALL Java_io_sentry_ndk_NativeModuleListLoader_nativeClearModuleList(JNIEnv *env, jclass cls) { sentry_clear_modulecache(); diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index c4d779a04e..e27c4d2022 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -61,6 +61,60 @@ static size_t walk_stack_with_dbghelp(HANDLE hProcess, DWORD crashed_tid, const CONTEXT *ctx_record, sentry_frame_info_t *frames, size_t max_frames); #endif +#if defined(SENTRY_PLATFORM_ANDROID) +typedef struct { + sentry_options_t *options; + sentry_crash_ipc_t *ipc; + FILE *log_file; + sentry_path_t *run_folder; + sentry_filelock_t *run_lock; + char envelope_path[SENTRY_CRASH_MAX_PATH]; + bool transport_shutdown; + bool run_folder_cleaned; +} sentry_android_pending_crash_daemon_t; + +static sentry_android_pending_crash_daemon_t g_android_pending_crash_daemon + = { 0 }; +static sentry_android_pending_crash_daemon_t *g_android_active_crash_daemon + = NULL; + +static void android_cleanup_pending_crash_daemon(bool clean_run_folder); + +__attribute__((visibility("default"))) char * +sentry_android_crash_daemon_run(pid_t app_pid, uint64_t app_tid, int notify_fd, + int ready_fd, const char *shm_path) +{ + if (!shm_path || !shm_path[0]) { + return NULL; + } + + int shm_fd = open(shm_path, O_RDWR | O_CLOEXEC); + if (shm_fd < 0) { + return NULL; + } + + memset(&g_android_pending_crash_daemon, 0, + sizeof(g_android_pending_crash_daemon)); + g_android_active_crash_daemon = &g_android_pending_crash_daemon; + + int rv = sentry__crash_daemon_main( + app_pid, app_tid, notify_fd, ready_fd, shm_fd); + g_android_active_crash_daemon = NULL; + + if (rv != 0 || !g_android_pending_crash_daemon.envelope_path[0]) { + android_cleanup_pending_crash_daemon(true); + return NULL; + } + + char *path + = sentry__string_clone(g_android_pending_crash_daemon.envelope_path); + if (!path) { + android_cleanup_pending_crash_daemon(true); + } + return path; +} +#endif + // Provide default ASAN options for sentry-crash daemon executable // This suppresses false positives from fork() which ASAN doesn't handle well #if defined(__has_feature) @@ -220,6 +274,77 @@ attachment_is_placeholder(const sentry_options_t *options, const char *path) return is_placeholder; } +#if defined(SENTRY_PLATFORM_ANDROID) +static sentry_path_t * +make_run_envelope_path( + const sentry_run_t *run, const sentry_envelope_t *envelope) +{ + if (!run || !envelope || !run->run_path) { + return NULL; + } + + sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); + if (sentry_uuid_is_nil(&event_id)) { + return NULL; + } + + char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); + if (!filename) { + return NULL; + } + + sentry_path_t *path = sentry__path_join_str(run->run_path, filename); + sentry_free(filename); + return path; +} + +static sentry_envelope_t * +wait_for_android_tombstone_envelope( + const sentry_path_t *native_envelope_path, sentry_path_t **merged_path_out) +{ + if (merged_path_out) { + *merged_path_out = NULL; + } + if (!native_envelope_path) { + return NULL; + } + + sentry_path_t *merged_path + = sentry__path_append_str(native_envelope_path, ".merged"); + if (!merged_path) { + return NULL; + } + + const int timeout_ms = 10000; + const int interval_ms = 100; + int elapsed_ms = 0; + while (elapsed_ms < timeout_ms) { + if (sentry__path_is_file(merged_path)) { + SENTRY_DEBUGF("Found Android tombstone merged envelope: %s", + merged_path->path); + sentry_envelope_t *envelope + = sentry__envelope_from_path(merged_path); + if (envelope) { + if (merged_path_out) { + *merged_path_out = merged_path; + } else { + sentry__path_free(merged_path); + } + return envelope; + } + SENTRY_WARN("Failed to read Android tombstone merged envelope"); + break; + } + usleep(interval_ms * 1000); + elapsed_ms += interval_ms; + } + + SENTRY_DEBUG("Timed out waiting for Android tombstone merged envelope"); + sentry__path_free(merged_path); + return NULL; +} +#endif + // For each large attachment listed in `/__sentry-attachments`, // cache it as an attachment-ref item. Small attachments were already inlined // during envelope writing. @@ -2876,10 +3001,11 @@ write_envelope_with_minidump(const sentry_options_t *options, * * Called by the crash daemon (out-of-process on Linux/macOS). */ -void +bool sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) { SENTRY_DEBUG("Processing crash - START"); + bool android_tombstone_sent = false; sentry_crash_context_t *ctx = ipc->shmem; @@ -3171,6 +3297,9 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) #if defined(SENTRY_PLATFORM_ANDROID) bool write_envelope = true; + sentry_path_t *android_native_envelope_path + = make_run_envelope_path(options ? options->run : NULL, envelope); + bool android_defer_envelope = g_android_active_crash_daemon != NULL; #else bool write_envelope = sentry__envelope_has_content_type( envelope, SENTRY_ATTACHMENT_REF_MIME); @@ -3193,8 +3322,40 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) #if defined(SENTRY_PLATFORM_ANDROID) SENTRY_DEBUG( "Crash envelope written for Android tombstone merging"); - sentry_envelope_free(envelope); - envelope = NULL; + sentry__atomic_store(&ctx->state, SENTRY_CRASH_STATE_DONE); + SENTRY_DEBUG("Released crashed process for Android tombstone"); + if (android_defer_envelope && android_native_envelope_path) { + strncpy(g_android_active_crash_daemon->envelope_path, + android_native_envelope_path->path, + sizeof(g_android_active_crash_daemon->envelope_path) + - 1); + g_android_active_crash_daemon->envelope_path + [sizeof(g_android_active_crash_daemon->envelope_path) + - 1] + = '\0'; + g_android_active_crash_daemon->run_folder = run_folder; + g_android_active_crash_daemon->run_lock = run_lock; + run_folder = NULL; + run_lock = NULL; + sentry_envelope_free(envelope); + envelope = NULL; + } else { + sentry_path_t *merged_path = NULL; + sentry_envelope_t *merged_envelope + = wait_for_android_tombstone_envelope( + android_native_envelope_path, &merged_path); + if (merged_envelope) { + sentry_envelope_free(envelope); + envelope = merged_envelope; + sentry__path_remove(android_native_envelope_path); + sentry__path_remove(merged_path); + sentry__path_free(merged_path); + android_tombstone_sent = true; + } else { + sentry_envelope_free(envelope); + envelope = NULL; + } + } #endif } } @@ -3210,6 +3371,10 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) } } +#if defined(SENTRY_PLATFORM_ANDROID) + sentry__path_free(android_native_envelope_path); +#endif + // Clean up temporary envelope file (keep minidump for // inspection/debugging) #if defined(SENTRY_PLATFORM_UNIX) @@ -3278,7 +3443,124 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) done: SENTRY_DEBUG("Processing crash - END"); SENTRY_DEBUG("Crash processing complete"); + return android_tombstone_sent; +} + +#if defined(SENTRY_PLATFORM_ANDROID) +__attribute__((visibility("default"))) bool +sentry_android_crash_daemon_send(const char *path, uint64_t timeout) +{ + sentry_android_pending_crash_daemon_t *daemon + = &g_android_pending_crash_daemon; + if (!daemon->options || !daemon->ipc || !path || !path[0]) { + android_cleanup_pending_crash_daemon(false); + return false; + } + + sentry_path_t *envelope_path = sentry__path_from_str(path); + sentry_envelope_t *envelope + = envelope_path ? sentry__envelope_from_path(envelope_path) : NULL; + if (envelope_path) { + sentry__path_free(envelope_path); + } + if (!envelope) { + SENTRY_WARN("Failed to read merged Android crash envelope"); + android_cleanup_pending_crash_daemon(false); + return false; + } + + bool sent = false; + if (daemon->options->transport && daemon->options->run) { + SENTRY_DEBUG("Capturing merged Android crash envelope"); + sentry__capture_envelope( + daemon->options->transport, envelope, daemon->options); + + sentry_path_t *raw_path = sentry__path_from_str(daemon->envelope_path); + if (raw_path) { + sentry__path_remove(raw_path); + sentry__path_free(raw_path); + } + if (strcmp(path, daemon->envelope_path) != 0) { + sentry_path_t *merged_path = sentry__path_from_str(path); + if (merged_path) { + sentry__path_remove(merged_path); + sentry__path_free(merged_path); + } + } + + if (daemon->run_folder) { + SENTRY_DEBUG("Cleaning up Android crash run folder"); + sentry__path_remove_all(daemon->run_folder); + daemon->run_folder_cleaned = true; + } + if (daemon->run_lock) { + sentry__filelock_unlock(daemon->run_lock); + sentry__filelock_free(daemon->run_lock); + daemon->run_lock = NULL; + } + + daemon->options->shutdown_timeout = timeout; + int rv = sentry__transport_shutdown( + daemon->options->transport, daemon->options->shutdown_timeout); + daemon->transport_shutdown = true; + if (rv != 0) { + SENTRY_WARN("transport did not shut down cleanly"); + } + size_t dumped_envelopes = sentry__transport_dump_queue( + daemon->options->transport, daemon->options->run); + if (rv == 0 && !dumped_envelopes && daemon->options->run) { + sentry__run_clean(daemon->options->run, true); + } + sent = rv == 0 && dumped_envelopes == 0; + } else { + SENTRY_WARN( + "No transport available for sending Android crash envelope"); + sentry_envelope_free(envelope); + } + + android_cleanup_pending_crash_daemon(false); + return sent; +} + +__attribute__((visibility("default"))) void +sentry_android_crash_daemon_close(void) +{ + android_cleanup_pending_crash_daemon(true); +} + +static void +android_cleanup_pending_crash_daemon(bool clean_run_folder) +{ + sentry_android_pending_crash_daemon_t *daemon + = &g_android_pending_crash_daemon; + + if (daemon->run_folder) { + if (clean_run_folder && !daemon->run_folder_cleaned) { + sentry__path_remove_all(daemon->run_folder); + } + sentry__path_free(daemon->run_folder); + } + if (daemon->run_lock) { + sentry__filelock_unlock(daemon->run_lock); + sentry__filelock_free(daemon->run_lock); + } + if (daemon->options) { + if (daemon->options->transport && !daemon->transport_shutdown) { + sentry__transport_shutdown(daemon->options->transport, 0); + } + sentry_options_free(daemon->options); + } + if (daemon->ipc) { + sentry__crash_ipc_unlink(daemon->ipc); + sentry__crash_ipc_free(daemon->ipc); + } + if (daemon->log_file) { + fclose(daemon->log_file); + } + + memset(daemon, 0, sizeof(*daemon)); } +#endif /** * Check if parent process is still alive @@ -3557,6 +3839,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, // Daemon main loop bool crash_processed = false; + bool android_tombstone_sent = false; while (true) { // Wait for crash notification (with timeout to check parent health) bool wait_result @@ -3571,7 +3854,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, long state = sentry__atomic_fetch(&ipc->shmem->state); if (state == SENTRY_CRASH_STATE_CRASHED && !crash_processed) { SENTRY_DEBUG("Crash notification received, processing"); - sentry__process_crash(options, ipc); + android_tombstone_sent = sentry__process_crash(options, ipc); crash_processed = true; // After processing crash, exit regardless of parent state @@ -3592,6 +3875,16 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, SENTRY_DEBUG("Daemon exiting"); +#if defined(SENTRY_PLATFORM_ANDROID) + if (g_android_active_crash_daemon + && g_android_active_crash_daemon->envelope_path[0]) { + g_android_active_crash_daemon->options = options; + g_android_active_crash_daemon->ipc = ipc; + g_android_active_crash_daemon->log_file = log_file; + return 0; + } +#endif + // Cleanup if (options) { size_t dumped_envelopes = 0; @@ -3607,7 +3900,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, options->transport, options->run); if (rv == 0 && !dumped_envelopes && options->run) { #if defined(SENTRY_PLATFORM_ANDROID) - if (!crash_processed) { + if (!crash_processed || android_tombstone_sent) { sentry__run_clean(options->run, true); } else { SENTRY_DEBUG( diff --git a/src/backends/native/sentry_crash_daemon.h b/src/backends/native/sentry_crash_daemon.h index 5228b8f8ad..8cbc8f799a 100644 --- a/src/backends/native/sentry_crash_daemon.h +++ b/src/backends/native/sentry_crash_daemon.h @@ -73,7 +73,7 @@ int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, * @param options Sentry options (DSN, transport, etc.) * @param ipc Crash IPC with crash context in shared memory */ -void sentry__process_crash( +bool sentry__process_crash( const struct sentry_options_s *options, sentry_crash_ipc_t *ipc); #endif diff --git a/src/backends/native/sentry_crash_ipc.c b/src/backends/native/sentry_crash_ipc.c index f404006e39..3c45024963 100644 --- a/src/backends/native/sentry_crash_ipc.c +++ b/src/backends/native/sentry_crash_ipc.c @@ -16,12 +16,12 @@ # include # include -sentry_crash_ipc_t * -sentry__crash_ipc_init_app( - const char *database_path, sentry_mutex_t *init_mutex) +static sentry_crash_ipc_t * +sentry__crash_ipc_init_app_at_path(const char *shm_path, int notify_fd, + int ready_fd, sentry_mutex_t *init_mutex) { - if (!database_path || !database_path[0]) { - SENTRY_WARN("Android crash IPC requires a database path"); + if (!shm_path || !shm_path[0]) { + SENTRY_WARN("Android crash IPC requires a shared memory path"); return NULL; } @@ -32,13 +32,10 @@ sentry__crash_ipc_init_app( ipc->is_daemon = false; ipc->init_mutex = init_mutex; ipc->shm_fd = -1; - ipc->notify_fd = -1; - ipc->ready_fd = -1; - - uint64_t tid = (uint64_t)pthread_self(); - uint32_t id = (uint32_t)((getpid() ^ (tid & 0xFFFFFFFF)) & 0xFFFFFFFF); - snprintf(ipc->shm_path, sizeof(ipc->shm_path), "%s/.sentry-shm-%08x", - database_path, id); + ipc->notify_fd = notify_fd; + ipc->ready_fd = ready_fd; + strncpy(ipc->shm_path, shm_path, sizeof(ipc->shm_path) - 1); + ipc->shm_path[sizeof(ipc->shm_path) - 1] = '\0'; if (ipc->init_mutex) { sentry__mutex_lock(ipc->init_mutex); @@ -84,16 +81,20 @@ sentry__crash_ipc_init_app( goto fail; } - ipc->notify_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); if (ipc->notify_fd < 0) { - SENTRY_WARNF("failed to create eventfd: %s", strerror(errno)); - goto fail; + ipc->notify_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (ipc->notify_fd < 0) { + SENTRY_WARNF("failed to create eventfd: %s", strerror(errno)); + goto fail; + } } - ipc->ready_fd = eventfd(0, EFD_CLOEXEC); if (ipc->ready_fd < 0) { - SENTRY_WARNF("failed to create ready eventfd: %s", strerror(errno)); - goto fail; + ipc->ready_fd = eventfd(0, EFD_CLOEXEC); + if (ipc->ready_fd < 0) { + SENTRY_WARNF("failed to create ready eventfd: %s", strerror(errno)); + goto fail; + } } if (!shm_exists) { @@ -136,6 +137,32 @@ sentry__crash_ipc_init_app( return NULL; } +sentry_crash_ipc_t * +sentry__crash_ipc_init_app( + const char *database_path, sentry_mutex_t *init_mutex) +{ + if (!database_path || !database_path[0]) { + SENTRY_WARN("Android crash IPC requires a database path"); + return NULL; + } + + uint64_t tid = (uint64_t)pthread_self(); + uint32_t id = (uint32_t)((getpid() ^ (tid & 0xFFFFFFFF)) & 0xFFFFFFFF); + char shm_path[SENTRY_CRASH_MAX_PATH] = { 0 }; + snprintf( + shm_path, sizeof(shm_path), "%s/.sentry-shm-%08x", database_path, id); + + return sentry__crash_ipc_init_app_at_path(shm_path, -1, -1, init_mutex); +} + +sentry_crash_ipc_t * +sentry__crash_ipc_init_app_with_fds(const char *shm_path, int notify_fd, + int ready_fd, sentry_mutex_t *init_mutex) +{ + return sentry__crash_ipc_init_app_at_path( + shm_path, notify_fd, ready_fd, init_mutex); +} + sentry_crash_ipc_t * sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd, int shm_fd) diff --git a/src/backends/native/sentry_crash_ipc.h b/src/backends/native/sentry_crash_ipc.h index 6c075951a2..d3bc238ae3 100644 --- a/src/backends/native/sentry_crash_ipc.h +++ b/src/backends/native/sentry_crash_ipc.h @@ -78,6 +78,8 @@ typedef struct { #if defined(SENTRY_PLATFORM_ANDROID) sentry_crash_ipc_t *sentry__crash_ipc_init_app( const char *database_path, sentry_mutex_t *init_mutex); +sentry_crash_ipc_t *sentry__crash_ipc_init_app_with_fds(const char *shm_path, + int notify_fd, int ready_fd, sentry_mutex_t *init_mutex); #elif defined(SENTRY_PLATFORM_LINUX) sentry_crash_ipc_t *sentry__crash_ipc_init_app(sem_t *init_sem); #elif defined(SENTRY_PLATFORM_MACOS) diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 853c0070e5..6fe340e4a3 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -58,6 +58,59 @@ static sem_t *g_ipc_init_sem = SEM_FAILED; static char g_ipc_sem_name[64] = { 0 }; #endif +#if defined(SENTRY_PLATFORM_ANDROID) +typedef struct { + bool configured; + char shm_path[SENTRY_CRASH_MAX_PATH]; + int notify_fd; + int ready_fd; +} android_crash_daemon_config_t; + +static android_crash_daemon_config_t g_android_crash_daemon_config + = { false, { 0 }, -1, -1 }; + +__attribute__((visibility("default"))) void +sentry_android_crash_daemon_init( + const char *shm_path, int notify_fd, int ready_fd) +{ + if (!shm_path || !shm_path[0] || notify_fd < 0 || ready_fd < 0) { + return; + } + + sentry__mutex_lock(&g_ipc_sync_mutex); + strncpy(g_android_crash_daemon_config.shm_path, shm_path, + sizeof(g_android_crash_daemon_config.shm_path) - 1); + g_android_crash_daemon_config + .shm_path[sizeof(g_android_crash_daemon_config.shm_path) - 1] + = '\0'; + g_android_crash_daemon_config.notify_fd = notify_fd; + g_android_crash_daemon_config.ready_fd = ready_fd; + g_android_crash_daemon_config.configured = true; + sentry__mutex_unlock(&g_ipc_sync_mutex); +} + +static bool +take_android_crash_daemon_config( + char *shm_path, size_t shm_path_len, int *notify_fd, int *ready_fd) +{ + bool configured = false; + sentry__mutex_lock(&g_ipc_sync_mutex); + if (g_android_crash_daemon_config.configured) { + strncpy( + shm_path, g_android_crash_daemon_config.shm_path, shm_path_len - 1); + shm_path[shm_path_len - 1] = '\0'; + *notify_fd = g_android_crash_daemon_config.notify_fd; + *ready_fd = g_android_crash_daemon_config.ready_fd; + g_android_crash_daemon_config.notify_fd = -1; + g_android_crash_daemon_config.ready_fd = -1; + g_android_crash_daemon_config.configured = false; + configured = true; + } + sentry__mutex_unlock(&g_ipc_sync_mutex); + return configured; +} +#endif + static char * resolve_handler_path(const sentry_options_t *options) { @@ -232,6 +285,7 @@ typedef struct { sentry_path_t *envelope_path; size_t num_breadcrumbs; volatile long crashed; + bool android_service_daemon; } native_backend_state_t; static int @@ -302,9 +356,20 @@ native_backend_startup( #elif defined(SENTRY_PLATFORM_IOS) state->ipc = sentry__crash_ipc_init_app(NULL); #elif defined(SENTRY_PLATFORM_ANDROID) - state->ipc = sentry__crash_ipc_init_app( - options->database_path ? options->database_path->path : NULL, - &g_ipc_sync_mutex); + char service_shm_path[SENTRY_CRASH_MAX_PATH] = { 0 }; + int service_notify_fd = -1; + int service_ready_fd = -1; + state->android_service_daemon + = take_android_crash_daemon_config(service_shm_path, + sizeof(service_shm_path), &service_notify_fd, &service_ready_fd); + if (state->android_service_daemon) { + state->ipc = sentry__crash_ipc_init_app_with_fds(service_shm_path, + service_notify_fd, service_ready_fd, &g_ipc_sync_mutex); + } else { + state->ipc = sentry__crash_ipc_init_app( + options->database_path ? options->database_path->path : NULL, + &g_ipc_sync_mutex); + } #elif defined(SENTRY_PLATFORM_MACOS) state->ipc = sentry__crash_ipc_init_app(&g_ipc_sync_mutex); #else @@ -508,22 +573,31 @@ native_backend_startup( #else // Other platforms: Use out-of-process daemon // Pass the notification handles (eventfd/pipe on Unix, events on Windows) - char *daemon_handler_path = resolve_handler_path(options); + char *daemon_handler_path = NULL; # if defined(SENTRY_PLATFORM_ANDROID) - uint64_t tid = (uint64_t)pthread_self(); - state->daemon_pid - = sentry__crash_daemon_start(getpid(), tid, state->ipc->notify_fd, - state->ipc->ready_fd, state->ipc->shm_fd, daemon_handler_path); + if (state->android_service_daemon) { + state->daemon_pid = 0; + SENTRY_DEBUG("using Android service-hosted crash daemon"); + } else { + daemon_handler_path = resolve_handler_path(options); + uint64_t tid = (uint64_t)pthread_self(); + state->daemon_pid + = sentry__crash_daemon_start(getpid(), tid, state->ipc->notify_fd, + state->ipc->ready_fd, state->ipc->shm_fd, daemon_handler_path); + } # elif defined(SENTRY_PLATFORM_LINUX) + daemon_handler_path = resolve_handler_path(options); uint64_t tid = (uint64_t)pthread_self(); state->daemon_pid = sentry__crash_daemon_start(getpid(), tid, state->ipc->notify_fd, state->ipc->ready_fd, daemon_handler_path); # elif defined(SENTRY_PLATFORM_MACOS) + daemon_handler_path = resolve_handler_path(options); 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); # elif defined(SENTRY_PLATFORM_WINDOWS) + daemon_handler_path = resolve_handler_path(options); uint64_t tid = (uint64_t)GetCurrentThreadId(); state->daemon_pid = sentry__crash_daemon_start(GetCurrentProcessId(), tid, state->ipc->event_handle, state->ipc->ready_event_handle, @@ -536,7 +610,7 @@ native_backend_startup( # if defined(SENTRY_PLATFORM_WINDOWS) if (state->daemon_pid == (pid_t)-1) { # else - if (state->daemon_pid < 0) { + if (!state->android_service_daemon && state->daemon_pid < 0) { # endif SENTRY_WARN("failed to start crash daemon"); sentry__crash_ipc_free(state->ipc); @@ -545,7 +619,9 @@ native_backend_startup( return 1; } - SENTRY_DEBUGF("crash daemon started with PID %d", state->daemon_pid); + if (!state->android_service_daemon) { + SENTRY_DEBUGF("crash daemon started with PID %d", state->daemon_pid); + } # if defined(SENTRY_PLATFORM_MACOS) // Close unused pipe ends in parent process @@ -565,12 +641,13 @@ native_backend_startup( // On Linux, allow the daemon to ptrace this process // This is required when Yama LSM ptrace_scope is enabled - if (prctl(PR_SET_PTRACER, state->daemon_pid, 0, 0, 0) != 0) { + if (!state->android_service_daemon + && prctl(PR_SET_PTRACER, state->daemon_pid, 0, 0, 0) != 0) { SENTRY_WARNF( "prctl(PR_SET_PTRACER) failed: %s - daemon may not be able to " "read process memory", strerror(errno)); - } else { + } else if (!state->android_service_daemon) { SENTRY_DEBUGF("Set daemon PID %d as ptracer", state->daemon_pid); } # endif @@ -590,7 +667,9 @@ native_backend_startup( if (sentry__crash_handler_init(state->ipc) < 0) { SENTRY_WARN("failed to initialize crash handler"); # if defined(SENTRY_PLATFORM_UNIX) - kill(state->daemon_pid, SIGTERM); + if (!state->android_service_daemon && state->daemon_pid > 0) { + kill(state->daemon_pid, SIGTERM); + } # elif defined(SENTRY_PLATFORM_WINDOWS) # if !defined(SENTRY_PLATFORM_XBOX) wer_unregister_module();