From 6f4dc994f73e67e243ca27541ea7b9a775929b86 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 14 Oct 2026 08:25:50 +0200 Subject: [PATCH 01/23] ci: Support v1 version branch Run CI on `v1` like on `master`, and dispatch console compatibility checks to the same-named console branch for commits cut from `v1`. --- .github/workflows/ci.yml | 1 + .github/workflows/codeql.yml | 4 ++-- .github/workflows/console-check.yml | 20 +++++++++++++++++-- .github/workflows/e2e-test.yml | 1 + .../workflows/enforce-license-compliance.yml | 4 ++-- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c4c8b238f..3fb191f789 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - v1 - "release/**" paths-ignore: - "*.md" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e123a80bec..e0802f108a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,10 +2,10 @@ name: 'CodeQL' on: push: - branches: [master] + branches: [master, v1] pull_request: # The branches below must be a subset of the branches above - branches: [master] + branches: [master, v1] schedule: - cron: '17 23 * * 3' diff --git a/.github/workflows/console-check.yml b/.github/workflows/console-check.yml index 5483fe751b..3e1aa9322c 100644 --- a/.github/workflows/console-check.yml +++ b/.github/workflows/console-check.yml @@ -15,6 +15,7 @@ on: push: branches: - master + - v1 - "release/**" concurrency: @@ -66,12 +67,26 @@ jobs: run: | if [[ "$EVENT_NAME" == "push" && "$REF" == refs/heads/release/* ]]; then echo "mode=FULL_TEST" >> "$GITHUB_OUTPUT" - elif [[ "$EVENT_NAME" == "push" && "$REF" == "refs/heads/master" ]]; then + elif [[ "$EVENT_NAME" == "push" && "$REF" == refs/heads/@(master|v1) ]]; then echo "mode=UNIT_TEST" >> "$GITHUB_OUTPUT" else echo "mode=BUILD_ONLY" >> "$GITHUB_OUTPUT" fi + - name: Resolve console branch + id: ref + env: + GH_TOKEN: ${{ github.token }} + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + ahead() { gh api "repos/$GITHUB_REPOSITORY/compare/$1...$SHA?per_page=1" --jq .ahead_by; } + ref=main nearest=$(ahead master) + # version branches build against the same console branch + for branch in v1; do + if n=$(ahead "$branch" 2>/dev/null) && [[ $n -lt $nearest ]]; then ref=$branch nearest=$n; fi + done + echo "console_ref=$ref" >> "$GITHUB_OUTPUT" + # gh workflow run sends a workflow_dispatch event to the console repo. # native-compat-check.yml must exist on the default branch (main) of # the console repo for this to work. @@ -84,11 +99,12 @@ jobs: CALLBACK_SHA: ${{ github.event.pull_request.head.sha || github.sha }} CALLBACK_CONTEXT: ${{ matrix.context }} MODE: ${{ steps.mode.outputs.mode }} + CONSOLE_REF: ${{ steps.ref.outputs.console_ref }} PR_NUMBER: ${{ github.event.pull_request.number || github.ref_name }} run: | gh workflow run native-compat-check.yml \ --repo "$MATRIX_REPO" \ - --ref main \ + --ref "$CONSOLE_REF" \ -f native_ref="$NATIVE_REF" \ -f callback_repo="$CALLBACK_REPO" \ -f callback_sha="$CALLBACK_SHA" \ diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 9226137d60..3f10722da1 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - v1 - "release/**" paths-ignore: - "*.md" diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index 158a26fcca..ed72c3379d 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -2,9 +2,9 @@ name: Enforce License Compliance on: push: - branches: [master, main, release/*] + branches: [master, main, v1, release/*] pull_request: - branches: [master, main] + branches: [master, main, v1] jobs: enforce-license-compliance: From 8dc1bfbb3fb14c14d291b68086ceb095cd466085 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 14 Oct 2026 09:32:17 +0200 Subject: [PATCH 02/23] ci: Drop v1 push filters The collective PR runs CI on every push to v1 already. --- .github/workflows/ci.yml | 1 - .github/workflows/codeql.yml | 2 +- .github/workflows/console-check.yml | 3 +-- .github/workflows/e2e-test.yml | 1 - .github/workflows/enforce-license-compliance.yml | 2 +- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fb191f789..3c4c8b238f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - v1 - "release/**" paths-ignore: - "*.md" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e0802f108a..c92aad87d8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,7 +2,7 @@ name: 'CodeQL' on: push: - branches: [master, v1] + branches: [master] pull_request: # The branches below must be a subset of the branches above branches: [master, v1] diff --git a/.github/workflows/console-check.yml b/.github/workflows/console-check.yml index 3e1aa9322c..5a395cd28b 100644 --- a/.github/workflows/console-check.yml +++ b/.github/workflows/console-check.yml @@ -15,7 +15,6 @@ on: push: branches: - master - - v1 - "release/**" concurrency: @@ -67,7 +66,7 @@ jobs: run: | if [[ "$EVENT_NAME" == "push" && "$REF" == refs/heads/release/* ]]; then echo "mode=FULL_TEST" >> "$GITHUB_OUTPUT" - elif [[ "$EVENT_NAME" == "push" && "$REF" == refs/heads/@(master|v1) ]]; then + elif [[ "$EVENT_NAME" == "push" && "$REF" == "refs/heads/master" ]]; then echo "mode=UNIT_TEST" >> "$GITHUB_OUTPUT" else echo "mode=BUILD_ONLY" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 3f10722da1..9226137d60 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - v1 - "release/**" paths-ignore: - "*.md" diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index ed72c3379d..7eaf0a0635 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -2,7 +2,7 @@ name: Enforce License Compliance on: push: - branches: [master, main, v1, release/*] + branches: [master, main, release/*] pull_request: branches: [master, main, v1] From 212b6a6c02c2cd3c652e47fd7a4f78018607458f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 14 Oct 2026 09:41:01 +0200 Subject: [PATCH 03/23] ci(codeql): Analyze v1 pushes Pull request analyses are stored against the merge ref, so PRs into v1 had no baseline and compared against master. --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c92aad87d8..e0802f108a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,7 +2,7 @@ name: 'CodeQL' on: push: - branches: [master] + branches: [master, v1] pull_request: # The branches below must be a subset of the branches above branches: [master, v1] From a6ee584790d8b68060fe64041a6c93640359e159 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 14 Oct 2026 12:43:44 +0200 Subject: [PATCH 04/23] feat(build): shared library version (#2103) Set CMake [`VERSION`](https://cmake.org/cmake/help/latest/prop_tgt/VERSION.html) and [`SOVERSION`](https://cmake.org/cmake/help/latest/prop_tgt/SOVERSION.html) properties. Close: #1428 --- CHANGELOG.md | 7 +++ CMakeLists.txt | 21 ++++++++ README.md | 9 +++- tests/{test_build_static.py => test_build.py} | 54 +++++++++++++++++++ tests/test_integration_crashpad.py | 9 +++- tests/test_integration_macos_sandbox.py | 20 ++++--- 6 files changed, 110 insertions(+), 10 deletions(-) rename tests/{test_build_static.py => test_build.py} (70%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57fb456055..6e32bfff62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +**Breaking / Important behavior changes**: + +- macOS/Linux: shared libraries now use ABI-versioned filenames, such as `libsentry.1.dylib` or `libsentry.so.1` for 1.x. Windows, Android, and static libraries remain unchanged. ([#2103](https://github.com/getsentry/sentry-native/pull/2103)) + - NOTE: Applications must package the real versioned library and its symlinks, including `libsentry.1.dylib` or `libsentry.so.1`, because they link to and load the ABI-versioned name. + ## 0.17.3 **Fixes**: diff --git a/CMakeLists.txt b/CMakeLists.txt index 990a132a92..695f7d53a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -384,6 +384,27 @@ add_library(sentry ${SENTRY_LIBRARY_TYPE} "${PROJECT_SOURCE_DIR}/src/sentry_mpac if (XBOX) set_target_properties(sentry PROPERTIES VS_USER_PROPS gdk_build.props) endif() + +set_target_properties(sentry PROPERTIES + VERSION "${SENTRY_VERSION_BASE}" + SOVERSION "${SENTRY_VERSION_MAJOR}") + +if(WIN32 AND SENTRY_BUILD_SHARED_LIBS) + set(_SENTRY_IMAGE_VERSION "${SENTRY_VERSION_MAJOR}.${SENTRY_VERSION_MINOR}") + if(CMAKE_GENERATOR MATCHES "^Visual Studio " AND CMAKE_VERSION VERSION_LESS 4.1) + # VS generators before CMake 4.1 ignore VERSION for the PE image header. + target_link_options(sentry PRIVATE "/VERSION:${_SENTRY_IMAGE_VERSION}") + elseif(MINGW) + include(CheckLinkerFlag) + check_linker_flag(C "LINKER:/version:0.0" _SENTRY_LINKER_SUPPORTS_PE_VERSION) + if(_SENTRY_LINKER_SUPPORTS_PE_VERSION) + # lld silently ignores CMake's GNU image-version flags. + target_link_options(sentry PRIVATE "LINKER:/version:${_SENTRY_IMAGE_VERSION}") + endif() + endif() + unset(_SENTRY_IMAGE_VERSION) +endif() + target_sources(sentry PRIVATE "${PROJECT_SOURCE_DIR}/include/sentry.h") add_library(sentry::sentry ALIAS sentry) diff --git a/README.md b/README.md index fc21a0cedf..b195b08b0b 100644 --- a/README.md +++ b/README.md @@ -123,12 +123,17 @@ install └── lib ├── cmake │ └── sentry - ├── libsentry.dylib - └── libsentry.dylib.dSYM + ├── libsentry.1.0.0.dylib + ├── libsentry.1.dylib -> libsentry.1.0.0.dylib + ├── libsentry.dylib -> libsentry.1.dylib + └── libsentry.1.0.0.dylib.dSYM ``` Please refer to the CMake Manual for more details. +Starting with 1.0.0, shared libraries on macOS and Linux use ABI-versioned +runtime names such as `libsentry.1.dylib` or `libsentry.so.1`. + **Android**: The CMake project can also be configured to correctly work with the Android NDK, diff --git a/tests/test_build_static.py b/tests/test_build.py similarity index 70% rename from tests/test_build_static.py rename to tests/test_build.py index 4dc15d6fdf..956a90789d 100644 --- a/tests/test_build_static.py +++ b/tests/test_build.py @@ -1,17 +1,71 @@ import subprocess import sys import os +import struct + import pytest + +from . import SENTRY_VERSION, lib_name from .conditions import ( has_breakpad, has_crashpad, has_native, + is_aix, is_android, is_qemu, is_wine, ) +def _assert_symlink(path, target): + assert path.is_symlink() + assert os.readlink(path) == target + + +def _assert_shared_version(directory): + major, minor, _ = SENTRY_VERSION.split(".") + + if sys.platform == "darwin": + real_name = f"libsentry.{SENTRY_VERSION}.dylib" + abi_name = f"libsentry.{major}.dylib" + _assert_symlink(directory / "libsentry.dylib", abi_name) + _assert_symlink(directory / abi_name, real_name) + output = subprocess.check_output( + ["otool", "-L", directory / real_name], text=True + ) + assert abi_name in output + assert f"compatibility version {major}.0.0" in output + assert f"current version {SENTRY_VERSION}" in output + elif sys.platform == "win32": + dll = directory / lib_name("sentry") + with dll.open("rb") as binary: + binary.seek(0x3C) + pe_offset = struct.unpack(" Date: Wed, 14 Oct 2026 12:43:44 +0200 Subject: [PATCH 05/23] ci: release archive version (#2105) * ci: include version in the release archive name Fixes: #745 * ci: keep a stable archive name for latest downloads Create a `sentry-native.zip` copy for the GCS `latest/` path so each release replaces the same object. Keep versioned archive names for GitHub and version-specific GCS paths. --- .craft.yml | 11 +++++++++-- .github/workflows/ci.yml | 6 +++++- CHANGELOG.md | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.craft.yml b/.craft.yml index a828802521..cfebd614a8 100644 --- a/.craft.yml +++ b/.craft.yml @@ -3,17 +3,23 @@ changelogPolicy: auto targets: - name: github includeNames: /^(sentry-native).*\.zip$/ + excludeNames: /^sentry-native\.zip$/ - name: registry sdks: github:getsentry/sentry-native: maven:io.sentry:sentry-native-ndk: - name: gcs - includeNames: /^(sentry-native\.zip)$/ + includeNames: /^sentry-native-\d+\.\d+\.\d+.*\.zip$/ bucket: sentry-sdk-assets paths: - path: /sentry-native/{{version}}/ metadata: cacheControl: public, max-age=2592000 + - name: gcs + id: latest + includeNames: /^sentry-native\.zip$/ + bucket: sentry-sdk-assets + paths: - path: /sentry-native/latest/ metadata: cacheControl: public, max-age=600 @@ -32,4 +38,5 @@ targets: batchType: android bundleIdPrefix: sentry-native-ndk- requireNames: - - /^sentry-native.zip$/ + - /^sentry-native-\d+\.\d+\.\d+.*\.zip$/ + - /^sentry-native\.zip$/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c4c8b238f..bf1b6eb94b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -582,8 +582,11 @@ jobs: - name: Create source archive run: | + version=$(sed -nE 's/^#[[:space:]]*define[[:space:]]+SENTRY_SDK_VERSION[[:space:]]+"([^"]+)".*/\1/p' include/sentry.h) + test -n "$version" rm -rf build .c* .e* .git* scripts Makefile external/breakpad/src/tools external/breakpad/src/processor - zip -r sentry-native.zip . + zip -r "sentry-native-$version.zip" . + cp "sentry-native-$version.zip" sentry-native.zip - name: Build NDK artifacts working-directory: ndk @@ -595,6 +598,7 @@ jobs: name: ${{ github.sha }} if-no-files-found: error path: | + sentry-native-*.zip sentry-native.zip ndk/lib/build/distributions/*.zip ndk/lib/build/intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e32bfff62..64b7a911d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - macOS/Linux: shared libraries now use ABI-versioned filenames, such as `libsentry.1.dylib` or `libsentry.so.1` for 1.x. Windows, Android, and static libraries remain unchanged. ([#2103](https://github.com/getsentry/sentry-native/pull/2103)) - NOTE: Applications must package the real versioned library and its symlinks, including `libsentry.1.dylib` or `libsentry.so.1`, because they link to and load the ABI-versioned name. +- Release archives now include the SDK version in the filename, such as `sentry-native-1.0.0.zip`. ([#2105](https://github.com/getsentry/sentry-native/pull/2105)) ## 0.17.3 From 67e0b6da7f4b7262d9a5e3edb0cea79ceed13b6c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 14 Oct 2026 14:42:30 +0200 Subject: [PATCH 06/23] feat!: make native the default backend (#2186) BREAKING CHANGE: Linux, macOS, and Windows builds now default to the `native` backend instead of `crashpad`. --- CHANGELOG.md | 1 + CMakeLists.txt | 2 +- CONTRIBUTING.md | 2 +- README.md | 14 +++++++------- include/sentry.h | 9 +++++---- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b7a911d0..16e252434c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Breaking / Important behavior changes**: +- The `native` backend is now the default on Linux, macOS, and Windows. ([#2186](https://github.com/getsentry/sentry-native/pull/2186)) - macOS/Linux: shared libraries now use ABI-versioned filenames, such as `libsentry.1.dylib` or `libsentry.so.1` for 1.x. Windows, Android, and static libraries remain unchanged. ([#2103](https://github.com/getsentry/sentry-native/pull/2103)) - NOTE: Applications must package the real versioned library and its symlinks, including `libsentry.1.dylib` or `libsentry.so.1`, because they link to and load the ABI-versioned name. - Release archives now include the SDK version in the filename, such as `sentry-native-1.0.0.zip`. ([#2105](https://github.com/getsentry/sentry-native/pull/2105)) diff --git a/CMakeLists.txt b/CMakeLists.txt index 695f7d53a0..752a1325e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -251,7 +251,7 @@ if(MSVC AND CMAKE_GENERATOR_TOOLSET MATCHES "_xp$") elseif(MSVC AND (XBOX OR WINGDK)) set(SENTRY_DEFAULT_BACKEND "breakpad") elseif((APPLE AND NOT IOS) OR WIN32 OR LINUX) - set(SENTRY_DEFAULT_BACKEND "crashpad") + set(SENTRY_DEFAULT_BACKEND "native") else() set(SENTRY_DEFAULT_BACKEND "inproc") endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b55a68afa..76e0de3f24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -204,7 +204,7 @@ The fixture currently supports the following commands: Only on Linux using crashpad: - `crashpad-wait-for-upload`: Couples application shutdown to complete the upload in the `crashpad_handler`. -Only on Windows using crashpad with its WER handler module: +Only on Windows using the native or crashpad backend with its WER handler module: - `fastfail`: Crashes the application using the `__fastfail` intrinsic directly, thus by-passing SEH. - `stack-buffer-overrun`: Triggers the Windows Control Flow Guard, which also fast fails and in turn by-passes SEH. diff --git a/README.md b/README.md index b195b08b0b..d941648a22 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Building the Breakpad and Crashpad backends requires a `C++17` compatible compil **Build example**: ```sh -# configure the cmake build into the `build` directory, with crashpad (on macOS) +# configure the cmake build into the `build` directory, with native (on macOS) $ cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo # build the project $ cmake --build build --parallel @@ -117,7 +117,7 @@ $ cmake --install build --prefix install --config RelWithDebInfo $ eza --tree install install ├── bin -│ └── crashpad_handler +│ └── sentry-crash ├── include │ └── sentry.h └── lib @@ -149,7 +149,7 @@ The `ndk` folder provides a Gradle project that adds a Java JNI layer for Androi **MinGW**: 64-bit is the only platform supported for now. -LLVM + Clang are mandatory here : they are required to generate .pdb files, used by Crashpad for the report generation. +LLVM + Clang are mandatory here : they are required to generate .pdb files, used to symbolicate crash reports. For your application to generate the appropriate .pdb output, you need to activate CodeView file format generation on your application target. To do so, update your own CMakeLists.txt with something like `target_compile_options(${yourApplicationTarget} PRIVATE -gcodeview)`. @@ -256,14 +256,14 @@ using `cmake -D BUILD_SHARED_LIBS=OFF ..`. Sentry can use different backends depending on the platform. - **crashpad**: This uses the out-of-process crashpad handler. It is currently - only supported on Desktop OSes and used as the default on Windows, Linux, and macOS. + only supported on Desktop OSes. - **breakpad**: This uses the in-process breakpad handler. It is currently only supported on Desktop OSs. - **inproc**: A small in-process handler that is supported on all platforms, and is used as a default on Android. - **native**: An out-of-process crash handler that uses a lightweight daemon to monitor the application, generate minidumps, and send crash reports. - Supports Linux, macOS, and Windows. Compatible with TSAN and ASAN + Supports Linux, macOS, and Windows, where it is the default. Compatible with TSAN and ASAN sanitizers. - **none**: This builds `sentry-native` without a backend, so it does not handle crashes. It is primarily used for tests. @@ -347,8 +347,8 @@ using `cmake -D BUILD_SHARED_LIBS=OFF ..`. | - none | ✓ | ✓ | ✓ | ☑ | ☑ | | | | | | | | | Backends | | | | | | -| - crashpad | ☑ | ☑ | ☑ | | | -| - native | ✓ | ✓ | ✓ | | | +| - crashpad | ✓ | ✓ | ✓ | | | +| - native | ☑ | ☑ | ☑ | | | | - breakpad | ✓ | ✓ | ✓ | (✓)* | (✓)* | | - inproc | ✓ | ✓ | ✓ | ☑ | | | - none | ✓ | ✓ | ✓ | ✓ | | diff --git a/include/sentry.h b/include/sentry.h index 9012500a66..d4f3065521 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2135,10 +2135,11 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_session_replay_duration( sentry_options_t *opts, uint32_t duration_ms); /** - * Sets the path to the crashpad handler if the crashpad backend is used. + * Sets the path to the crash handler if the native or crashpad backend is used. * - * The path defaults to the `crashpad_handler`/`crashpad_handler.exe` - * executable in the same directory as the application executable. + * The path defaults to the `sentry-crash`/`sentry-crash.exe` executable for the + * native backend or `crashpad_handler`/`crashpad_handler.exe` for the crashpad + * backend, in the same directory as the application executable. * * Meaning if your application resides in * @@ -2146,7 +2147,7 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_session_replay_duration( * * then the handler path will be set (by default) to * - * "C:\path\to\your\crashpad_handler.exe" + * "C:\path\to\your\sentry-crash.exe" when using the native backend. * * It is recommended that library users set an explicit handler path, depending * on the directory/executable structure of their app. From 6dcddc8ff47ac5806d2dbac8a02c7c2412bda345 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 15 Oct 2026 09:06:38 +0200 Subject: [PATCH 07/23] ci: Publish v1 benchmarks separately --- .github/workflows/benchmark.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index f3de67091b..f039f38e62 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - v1 paths-ignore: - "*.md" pull_request: @@ -59,5 +60,5 @@ jobs: tool: customSmallerIsBetter output-file-path: benchmark.json github-token: ${{ secrets.GITHUB_TOKEN }} - benchmark-data-dir-path: ./ + benchmark-data-dir-path: ${{ github.ref_name == 'v1' && 'v1/' || './' }} auto-push: true From 3d260778b8358630ec44252f685f0153c93cea42 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 15 Oct 2026 11:49:48 +0200 Subject: [PATCH 08/23] ref: stabilize screenshot APIs (#2204) The custom capture hook required private SDK types, while screenshot options were still experimental. BREAKING CHANGE: Implement sentry_screenshot_capture(const char *, uint32_t), returning 0 on success. --- README.md | 9 +++++++++ include/sentry.h | 7 +++---- src/backends/native/sentry_crash_daemon.c | 5 +++-- src/backends/sentry_backend_breakpad.cpp | 4 ++-- src/backends/sentry_backend_inproc.c | 4 ++-- src/screenshot/sentry_screenshot_none.c | 7 +++---- src/screenshot/sentry_screenshot_windows.c | 20 +++++++++++++------- src/sentry_screenshot.h | 4 ++-- 8 files changed, 37 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index d941648a22..3673c3eae8 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,15 @@ using `cmake -D BUILD_SHARED_LIBS=OFF ..`. - **none**: Do not build any HTTP transport. This should be used if users want to handle uploads themselves +- `SENTRY_SCREENSHOT` (Default: `windows` on Windows, `none` elsewhere): + Selects the screenshot implementation. Set it to `custom` and provide an + external `int sentry_screenshot_capture(const char *path, uint32_t pid)` + implementation (with `extern "C"` linkage if written in C++). + Save a PNG screenshot to `path` and return `0` on success, nonzero on failure. + `pid` selects whose windows to capture: `0` for the current process, or the + crashed application's PID when capture runs in a separate daemon. The path is + UTF-8 on Windows. + - `SENTRY_BACKEND` (Default: depending on platform): Sentry can use different backends depending on the platform. diff --git a/include/sentry.h b/include/sentry.h index d4f3065521..f3c600c397 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2080,7 +2080,7 @@ SENTRY_API void sentry_options_add_view_hierarchy_n( * To decide per-event whether a screenshot should be captured, set a * `before_screenshot` callback via `sentry_options_set_before_screenshot`. */ -SENTRY_EXPERIMENTAL_API void sentry_options_set_attach_screenshot( +SENTRY_API void sentry_options_set_attach_screenshot( sentry_options_t *opts, int val); /** @@ -2109,9 +2109,8 @@ typedef int (*sentry_before_screenshot_function_t)( * See the `sentry_before_screenshot_function_t` typedef above for more * information. */ -SENTRY_EXPERIMENTAL_API void sentry_options_set_before_screenshot( - sentry_options_t *opts, sentry_before_screenshot_function_t func, - void *user_data); +SENTRY_API void sentry_options_set_before_screenshot(sentry_options_t *opts, + sentry_before_screenshot_function_t func, void *user_data); /** * Enables capturing a short retroactive video replay on crash. Currently only diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 79477d122c..65a70b85f5 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -4190,8 +4190,9 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) if (screenshot_path) { // Pass the crashed app's PID so we capture its windows, not the // daemon's - if (sentry__screenshot_capture( - screenshot_path, (uint32_t)ctx->crashed_pid)) { + if (sentry_screenshot_capture( + screenshot_path->path, (uint32_t)ctx->crashed_pid) + == 0) { SENTRY_DEBUG("Screenshot captured successfully"); } else { SENTRY_DEBUG("Screenshot capture failed"); diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index 655fc59edf..c801f652ca 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -237,8 +237,8 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, sentry_path_t *path = sentry__screenshot_get_path(options); sentry_value_t screenshot = sentry_attachment_from_file(path ? path->path : nullptr); - if (!sentry_value_is_null(screenshot) - && sentry__screenshot_capture(path, 0)) { + if (path && !sentry_value_is_null(screenshot) + && sentry_screenshot_capture(path->path, 0) == 0) { sentry__envelope_add_attachment(envelope, screenshot); } sentry_value_decref(screenshot); diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 3173254d8d..dbc454ef2c 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -806,8 +806,8 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, sentry_path_t *path = sentry__screenshot_get_path(options); sentry_value_t screenshot = sentry_attachment_from_file(path ? path->path : NULL); - if (!sentry_value_is_null(screenshot) - && sentry__screenshot_capture(path, 0)) { + if (path && !sentry_value_is_null(screenshot) + && sentry_screenshot_capture(path->path, 0) == 0) { sentry__envelope_add_attachment(envelope, screenshot); } sentry_value_decref(screenshot); diff --git a/src/screenshot/sentry_screenshot_none.c b/src/screenshot/sentry_screenshot_none.c index 01d3887087..538f33cd83 100644 --- a/src/screenshot/sentry_screenshot_none.c +++ b/src/screenshot/sentry_screenshot_none.c @@ -2,9 +2,8 @@ #include "sentry_core.h" -bool -sentry__screenshot_capture( - const sentry_path_t *UNUSED(path), uint32_t UNUSED(pid)) +int +sentry_screenshot_capture(const char *UNUSED(path), uint32_t UNUSED(pid)) { - return false; + return 1; } diff --git a/src/screenshot/sentry_screenshot_windows.c b/src/screenshot/sentry_screenshot_windows.c index 7064662265..a0eb3263ee 100644 --- a/src/screenshot/sentry_screenshot_windows.c +++ b/src/screenshot/sentry_screenshot_windows.c @@ -147,9 +147,13 @@ calculate_region(DWORD pid, HRGN region) } } -bool -sentry__screenshot_capture(const sentry_path_t *path, uint32_t pid) +int +sentry_screenshot_capture(const char *path, uint32_t pid) { + sentry_path_t *screenshot_path = sentry__path_from_str(path); + if (!screenshot_path) { + return 1; + } // Use provided PID, or current process if 0 DWORD target_pid = pid ? pid : GetCurrentProcessId(); HRGN region = CreateRectRgn(0, 0, 0, 0); @@ -162,7 +166,8 @@ sentry__screenshot_capture(const sentry_path_t *path, uint32_t pid) if (width <= 0 || height <= 0) { SENTRY_INFO("no visible windows to capture"); DeleteObject(region); - return false; + sentry__path_free(screenshot_path); + return 1; } HDC src = GetDC(NULL); @@ -173,16 +178,17 @@ sentry__screenshot_capture(const sentry_path_t *path, uint32_t pid) SelectClipRgn(hdc, region); BitBlt(hdc, 0, 0, width, height, src, box.left, box.top, SRCCOPY); - bool rv = save_bitmap(bitmap, path->path_w); + bool rv = save_bitmap(bitmap, screenshot_path->path_w); if (!rv) { - SENTRY_WARNF("Failed to save screenshot: \"%s\"", path->path); + SENTRY_WARNF("Failed to save screenshot: \"%s\"", path); } else { - SENTRY_DEBUGF("Saved screenshot: \"%s\"", path->path); + SENTRY_DEBUGF("Saved screenshot: \"%s\"", path); } DeleteObject(bitmap); DeleteDC(hdc); ReleaseDC(NULL, src); DeleteObject(region); - return rv; + sentry__path_free(screenshot_path); + return rv ? 0 : 1; } diff --git a/src/sentry_screenshot.h b/src/sentry_screenshot.h index b0ee209217..c372d34b6e 100644 --- a/src/sentry_screenshot.h +++ b/src/sentry_screenshot.h @@ -13,9 +13,9 @@ * @param pid The process ID whose windows should be captured (0 = current * process). * - * Returns true if the screenshot was successfully captured and saved. + * Returns 0 if the screenshot was successfully captured and saved. */ -bool sentry__screenshot_capture(const sentry_path_t *path, uint32_t pid); +int sentry_screenshot_capture(const char *path, uint32_t pid); /** * Returns the path where a screenshot should be saved. From e7c605800aa4025aa003376263bd6d97f3ac9217 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 15 Oct 2026 14:06:20 +0200 Subject: [PATCH 09/23] ref(transport)!: rename and document custom transport factory (#2131) The factory used a private-style `sentry__`-prefixed name even though custom transports must provide its implementation. Give it a public-style name and document the contract for custom implementations. BREAKING CHANGE: Custom transport implementations must define: ``` sentry_transport_t *sentry_transport_new_default(void) ``` --- README.md | 5 +++++ src/sentry_options.c | 2 +- src/sentry_transport.h | 2 +- src/transports/sentry_http_transport_curl.c | 2 +- src/transports/sentry_http_transport_winhttp.c | 2 +- src/transports/sentry_transport_none.c | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3673c3eae8..798b60c8a9 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,11 @@ using `cmake -D BUILD_SHARED_LIBS=OFF ..`. Windows and is the default there. - **none**: Do not build any HTTP transport. This should be used if users want to handle uploads themselves + - **custom**: Provide an external + `sentry_transport_t *sentry_transport_new_default(void)` implementation + (with `extern "C"` linkage if written in C++). Return a transport created + with `sentry_transport_new` or `sentry_http_transport_new`, or `NULL` to + disable uploads. - `SENTRY_SCREENSHOT` (Default: `windows` on Windows, `none` elsewhere): Selects the screenshot implementation. Set it to `custom` and provide an diff --git a/src/sentry_options.c b/src/sentry_options.c index 6504cf5d42..54c01d0512 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -107,7 +107,7 @@ sentry_options_new(void) false; #endif opts->backend = sentry__backend_new(); - opts->transport = sentry__transport_new_default(); + opts->transport = sentry_transport_new_default(); opts->refcount = 1; opts->shutdown_timeout = SENTRY_DEFAULT_SHUTDOWN_TIMEOUT; opts->transfer_timeout = SENTRY_DEFAULT_TRANSFER_TIMEOUT; diff --git a/src/sentry_transport.h b/src/sentry_transport.h index 4c4dd0ef01..afe7fdb463 100644 --- a/src/sentry_transport.h +++ b/src/sentry_transport.h @@ -46,7 +46,7 @@ int sentry__transport_shutdown(sentry_transport_t *transport, uint64_t timeout); /** * This will create a new platform specific HTTP transport. */ -sentry_transport_t *sentry__transport_new_default(void); +sentry_transport_t *sentry_transport_new_default(void); /** * This function will instruct the platform specific transport to dump all the diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 32296facb8..79c0846751 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -547,7 +547,7 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, } sentry_transport_t * -sentry__transport_new_default(void) +sentry_transport_new_default(void) { SENTRY_INFO("initializing curl transport"); curl_client_t *client = curl_client_new(); diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index cc9db472d2..63a5ed6a68 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -410,7 +410,7 @@ exit:; } sentry_transport_t * -sentry__transport_new_default(void) +sentry_transport_new_default(void) { SENTRY_INFO("initializing winhttp transport"); winhttp_client_t *client = winhttp_client_new(); diff --git a/src/transports/sentry_transport_none.c b/src/transports/sentry_transport_none.c index 361a947e76..e9553e4cda 100644 --- a/src/transports/sentry_transport_none.c +++ b/src/transports/sentry_transport_none.c @@ -2,7 +2,7 @@ #include "sentry_transport.h" sentry_transport_t * -sentry__transport_new_default(void) +sentry_transport_new_default(void) { return NULL; } From a0e256402b8aa6b7f0914d62e87d2dac0eb6c26f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 15 Oct 2026 15:06:30 +0200 Subject: [PATCH 10/23] perf: add support for batched scope updates (#2107) Keep the scope write lock across a batch while allowing the owning thread to use existing scope accessors without relocking. Defer backend scope flushes until the outermost batch completes. --- CHANGELOG.md | 4 + include/sentry.h | 63 ++++++++++++ src/sentry_scope.c | 196 +++++++++++++++++++++++++++++++++--- tests/unit/test_scope.c | 213 ++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 5 + 5 files changed, 468 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cba9de7355..0e45a2f686 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ - NOTE: Applications must package the real versioned library and its symlinks, including `libsentry.1.dylib` or `libsentry.so.1`, because they link to and load the ABI-versioned name. - Release archives now include the SDK version in the filename, such as `sentry-native-1.0.0.zip`. ([#2105](https://github.com/getsentry/sentry-native/pull/2105)) +**Features**: + +- Add `sentry_scope_begin_read`/`sentry_scope_end_read` and `sentry_scope_begin_write`/`sentry_scope_end_write` for consistent reads of multiple scope properties and applying multiple scope changes with a single scope flush. ([#2107](https://github.com/getsentry/sentry-native/pull/2107)) + ## Unreleased **Features**: diff --git a/include/sentry.h b/include/sentry.h index f3c600c397..f703b27ee0 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2633,6 +2633,69 @@ SENTRY_API sentry_scope_t *sentry_local_scope_new(void); */ SENTRY_API sentry_scope_t *sentry_scope_new(void); +/** + * Begins a batch read of `scope`. + * + * Tip: Individual scope property reads acquire a read lock automatically. + * Use this function to batch multiple reads under one read lock. + * + * The scope remains read-locked until the matching `sentry_scope_end_read` call + * on the same thread, so multiple properties can be read consistently. Other + * threads may read the scope concurrently, but cannot modify it until the read + * lock is released. Reads may be nested for the same scope, including inside a + * write batch, which keeps its write lock. + * + * Warning: A read lock allows only reads. Use `sentry_scope_begin_write` if + * you need to modify the scope. Calling a scope-mutating function while + * read-locked triggers an assertion in debug builds and may deadlock in + * release builds. + * + * Returns 0 on success or 1 if `scope` is NULL or allocation fails. Each + * successful call must be matched by `sentry_scope_end_read` before freeing the + * scope. + */ +SENTRY_API int sentry_scope_begin_read(const sentry_scope_t *scope); + +/** + * Ends a read batch begun by `sentry_scope_begin_read`. + * + * The final matching call releases the scope read lock. Reads nested inside a + * write batch leave the scope write-locked until the final matching + * `sentry_scope_end_write`. This must be called on the same thread as the + * matching begin call. + */ +SENTRY_API void sentry_scope_end_read(const sentry_scope_t *scope); + +/** + * Begins a batch write of `scope`. + * + * Tip: Individual scope property accesses acquire the appropriate read or + * write lock automatically. Use this function to batch multiple reads, writes, + * or both under one write lock. + * + * The scope remains write-locked until the matching + * `sentry_scope_end_write` call on the same thread. A write lock allows reads + * and writes. Writes may be nested for the same scope. + * + * Note: A read lock cannot be upgraded to a write lock. If this thread already + * holds a read lock on the scope, this function returns 1 without changing + * the lock. + * + * Returns 0 on success or 1 if `scope` is NULL, allocation fails, or the + * calling thread already holds a read lock on the scope. Each successful call + * must be matched by `sentry_scope_end_write` before freeing the scope. + */ +SENTRY_API int sentry_scope_begin_write(sentry_scope_t *scope); + +/** + * Ends a write batch begun by `sentry_scope_begin_write`. + * + * The final matching call releases the scope write lock and flushes any + * pending global scope changes to the backend. This must be called on the + * same thread as the matching begin call. + */ +SENTRY_API void sentry_scope_end_write(sentry_scope_t *scope); + /** * Frees a scope created via `sentry_scope_new`, `sentry_scope_clone`, or * `sentry_local_scope_new`. diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 3f568fcb88..638a01e8ae 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -61,6 +61,13 @@ struct sentry_scope_data_s { bool trace_managed; }; +typedef struct scope_access_s { + sentry_scope_data_t *data; + size_t depth; + bool write; + struct scope_access_s *next; +} scope_access_t; + static bool g_scope_initialized = false; static sentry_scope_t g_scope = { 0 }; static sentry_scope_data_t g_scope_data = { 0 }; @@ -68,8 +75,10 @@ static bool g_scope_idle_initialized = false; static sentry_cond_t g_scope_idle; #ifdef _MSC_VER static __declspec(thread) size_t g_scope_depth; +static __declspec(thread) scope_access_t *g_scope_access; #else static __thread size_t g_scope_depth; +static __thread scope_access_t *g_scope_access; #endif #ifdef SENTRY__MUTEX_INIT_DYN SENTRY__MUTEX_INIT_DYN(g_lock) @@ -77,23 +86,79 @@ SENTRY__MUTEX_INIT_DYN(g_lock) static sentry_mutex_t g_lock = SENTRY__MUTEX_INIT; #endif +static scope_access_t * +scope_data_get_access(const sentry_scope_data_t *data) +{ +#ifndef SENTRY_PLATFORM_WINDOWS + // avoid dynamic TLS access during signal handling + if (!sentry__block_for_signal_handler()) { + return NULL; + } +#endif + for (scope_access_t *access = g_scope_access; access; + access = access->next) { + if (access->data == data) { + return access; + } + } + return NULL; +} + +static bool +scope_data_read_lock(const sentry_scope_data_t *data) +{ + if (scope_data_get_access(data)) { + return false; + } + sentry__rwlock_read_lock((sentry_rwlock_t *)&data->rwlock); + return true; +} + +static void +scope_data_read_unlock(const sentry_scope_data_t *data, bool locked) +{ + if (locked) { + sentry__rwlock_read_unlock((sentry_rwlock_t *)&data->rwlock); + } +} + +static bool +scope_data_write_lock(sentry_scope_data_t *data) +{ + scope_access_t *access = scope_data_get_access(data); + assert(!access || access->write); + if (access && access->write) { + return false; + } + sentry__rwlock_write_lock(&data->rwlock); + return true; +} + +static void +scope_data_write_unlock(sentry_scope_data_t *data, bool locked) +{ + if (locked) { + sentry__rwlock_write_unlock(&data->rwlock); + } +} + #define SENTRY_SCOPE_READ_LOCK(Data) \ for (const sentry_scope_data_t *_locked_data = (Data); _locked_data; \ - sentry__rwlock_read_unlock((sentry_rwlock_t *)&_locked_data->rwlock), \ - _locked_data = NULL) \ - for (bool _locked_once \ - = (sentry__rwlock_read_lock( \ - (sentry_rwlock_t *)&_locked_data->rwlock), \ - true); \ - _locked_once; _locked_once = false) + _locked_data = NULL) \ + for (bool _lock_acquired = scope_data_read_lock(_locked_data), \ + _locked_once = true; \ + _locked_once; \ + scope_data_read_unlock(_locked_data, _lock_acquired), \ + _locked_once = false) #define SENTRY_SCOPE_WRITE_LOCK(Data) \ for (sentry_scope_data_t *_locked_data = (Data); _locked_data; \ - sentry__rwlock_write_unlock(&_locked_data->rwlock), \ - _locked_data = NULL) \ - for (bool _locked_once \ - = (sentry__rwlock_write_lock(&_locked_data->rwlock), true); \ - _locked_once; _locked_once = false) + _locked_data = NULL) \ + for (bool _lock_acquired = scope_data_write_lock(_locked_data), \ + _locked_once = true; \ + _locked_once; \ + scope_data_write_unlock(_locked_data, _lock_acquired), \ + _locked_once = false) static size_t begin_scope_notify(sentry_scope_t *scope); static void end_scope_notify(sentry_scope_t *scope); @@ -543,7 +608,9 @@ flush_scope(sentry_scope_t *scope) } sentry__mutex_lock(&scope->observers_lock); - if (scope->pending_flush && !scope->is_notifying) { + scope_access_t *access = scope_data_get_access(scope->data); + if (scope->pending_flush && !scope->is_notifying + && (!access || !access->write)) { scope->pending_flush = false; if (scope->flush_func) { scope->flush_func(scope); @@ -715,6 +782,109 @@ sentry_scope_new(void) return scope; } +static int +scope_begin(const sentry_scope_t *scope, bool write) +{ + if (!scope) { + return 1; + } +#ifndef SENTRY_PLATFORM_WINDOWS + if (!sentry__block_for_signal_handler()) { + return 0; + } +#endif + + sentry_scope_data_t *data = scope->data; + scope_access_t *access = scope_data_get_access(data); + if (access) { + if (write && !access->write) { + return 1; + } + access->depth++; + return 0; + } + + access = SENTRY_MAKE(scope_access_t); + if (!access) { + return 1; + } + + if (write) { + lock_scope_notify((sentry_scope_t *)scope); + sentry__rwlock_write_lock(&data->rwlock); + } else { + sentry__rwlock_read_lock(&data->rwlock); + } + access->data = data; + access->depth = 1; + access->write = write; + access->next = g_scope_access; + g_scope_access = access; + return 0; +} + +int +sentry_scope_begin_read(const sentry_scope_t *scope) +{ + return scope_begin(scope, false); +} + +int +sentry_scope_begin_write(sentry_scope_t *scope) +{ + return scope_begin(scope, true); +} + +static void +scope_end(const sentry_scope_t *scope) +{ + if (!scope) { + return; + } +#ifndef SENTRY_PLATFORM_WINDOWS + if (!sentry__block_for_signal_handler()) { + return; + } +#endif + + scope_access_t **link = &g_scope_access; + while (*link && (*link)->data != scope->data) { + link = &(*link)->next; + } + scope_access_t *access = *link; + if (!access) { + assert(false); + return; + } + if (--access->depth > 0) { + return; + } + + *link = access->next; + bool write = access->write; + sentry_free(access); + if (!write) { + sentry__rwlock_read_unlock(&scope->data->rwlock); + return; + } + sentry__rwlock_write_unlock(&scope->data->rwlock); + + flush_scope((sentry_scope_t *)scope); + unlock_scope_notify((sentry_scope_t *)scope); +} + +void +sentry_scope_end_read(const sentry_scope_t *scope) +{ + scope_end(scope); +} + +void +sentry_scope_end_write(sentry_scope_t *scope) +{ + scope_end(scope); +} + void sentry_scope_free(sentry_scope_t *scope) { diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 2fbfad2f94..579c13b28f 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -35,6 +35,85 @@ scope_value_get_length(scope_value_getter_t get, const sentry_scope_t *scope) return length; } +SENTRY_TEST(scope_write_nested) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + + TEST_ASSERT(sentry_scope_begin_write(scope) == 0); + sentry_scope_set_tag(scope, "first", "value"); + + TEST_ASSERT(sentry_scope_begin_write(scope) == 0); + sentry_scope_set_tag(scope, "second", "value"); + TEST_ASSERT(sentry_scope_begin_read(scope) == 0); + TEST_CHECK(scope_value_get_length(sentry__scope_load_tags, scope) == 2); + sentry_scope_end_read(scope); + sentry_scope_end_write(scope); + + sentry_scope_set_environment(scope, "production"); + sentry_value_t environment = sentry__scope_ref_environment(scope); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(environment), "production"); + sentry_value_decref(environment); + sentry_scope_end_write(scope); + + TEST_CHECK(scope_value_get_length(sentry__scope_load_tags, scope) == 2); + sentry_scope_free(scope); +} + +typedef struct { + sentry_scope_t *scope; + sentry_waitable_flag_t started; + sentry_waitable_flag_t finished; + sentry_value_t environment; + sentry_value_t tag; + int result; +} scope_access_thread_t; + +SENTRY_THREAD_FN +read_scope_batch(void *data) +{ + scope_access_thread_t *state = data; + sentry__waitable_flag_set(&state->started); + state->result = sentry_scope_begin_read(state->scope); + if (state->result == 0) { + state->environment = sentry__scope_ref_environment(state->scope); + state->tag = scope_value_get_by_key( + sentry__scope_load_tags, state->scope, "version"); + sentry_scope_end_read(state->scope); + } + sentry__waitable_flag_set(&state->finished); + return 0; +} + +SENTRY_TEST(scope_write_exclusive) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + + scope_access_thread_t state = { .scope = scope }; + sentry__waitable_flag_init(&state.started); + sentry__waitable_flag_init(&state.finished); + + TEST_ASSERT(sentry_scope_begin_write(scope) == 0); + sentry_scope_set_environment(scope, "new"); + sentry_threadid_t thread; + sentry__thread_init(&thread); + TEST_ASSERT(!sentry__thread_spawn(&thread, read_scope_batch, &state)); + TEST_ASSERT(sentry__waitable_flag_wait(&state.started, 1000)); + TEST_CHECK(!sentry__waitable_flag_wait(&state.finished, 10)); + + sentry_scope_set_tag(scope, "version", "new"); + sentry_scope_end_write(scope); + TEST_CHECK(sentry__waitable_flag_wait(&state.finished, 1000)); + sentry__thread_join(thread); + TEST_CHECK_INT_EQUAL(state.result, 0); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(state.environment), "new"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(state.tag), "new"); + sentry_value_decref(state.environment); + sentry_value_decref(state.tag); + sentry_scope_free(scope); +} + SENTRY_TEST(scope_contexts) { SENTRY_TEST_OPTIONS_NEW(options); @@ -2063,6 +2142,23 @@ SENTRY_TEST(scope_observer_deferred_flush) TEST_CHECK_INT_EQUAL(observer_data.nested_flush_count, 0); TEST_CHECK_INT_EQUAL(observer_data.total_flush_count, 1); + observer_data.total_flush_count = 0; + observer_data.nested_flush_count = 0; + sentry_scope_t *scope = sentry__scope_getref(); + TEST_ASSERT(sentry_scope_begin_write(scope) == 0); + sentry_scope_set_tag(scope, "batched", "value"); + TEST_CHECK_INT_EQUAL(observer_data.nested_flush_count, 0); + TEST_CHECK_INT_EQUAL(observer_data.total_flush_count, 0); + TEST_ASSERT(sentry_scope_begin_read(scope) == 0); + sentry_scope_end_read(scope); + TEST_CHECK_INT_EQUAL(observer_data.total_flush_count, 0); + sentry_scope_end_write(scope); + TEST_CHECK_INT_EQUAL(observer_data.total_flush_count, 1); + TEST_ASSERT(sentry_scope_begin_read(scope) == 0); + sentry_scope_end_read(scope); + TEST_CHECK_INT_EQUAL(observer_data.total_flush_count, 1); + sentry__scope_finish(scope); + sentry_close(); } @@ -3493,3 +3589,120 @@ SENTRY_TEST(scope_clone_keeps_bound_span) sentry_close(); } + +SENTRY_TEST(scope_read_shared) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + sentry_scope_set_environment(scope, "old"); + sentry_scope_set_tag(scope, "version", "old"); + + scope_access_thread_t state = { .scope = scope }; + sentry__waitable_flag_init(&state.started); + sentry__waitable_flag_init(&state.finished); + + TEST_ASSERT(sentry_scope_begin_read(scope) == 0); + sentry_threadid_t thread; + sentry__thread_init(&thread); + TEST_ASSERT(!sentry__thread_spawn(&thread, read_scope_batch, &state)); + TEST_CHECK(sentry__waitable_flag_wait(&state.finished, 1000)); + sentry_scope_end_read(scope); + sentry__thread_join(thread); + + TEST_CHECK_INT_EQUAL(state.result, 0); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(state.environment), "old"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(state.tag), "old"); + sentry_value_decref(state.environment); + sentry_value_decref(state.tag); + sentry_scope_free(scope); +} + +SENTRY_THREAD_FN +write_scope_during_read(void *data) +{ + scope_access_thread_t *state = data; + sentry__waitable_flag_set(&state->started); + state->result = sentry_scope_begin_write(state->scope); + if (state->result == 0) { + sentry_scope_set_environment(state->scope, "new"); + sentry_scope_set_tag(state->scope, "version", "new"); + sentry_scope_end_write(state->scope); + } + sentry__waitable_flag_set(&state->finished); + return 0; +} + +SENTRY_TEST(scope_read_blocks_write) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + sentry_scope_set_environment(scope, "old"); + sentry_scope_set_tag(scope, "version", "old"); + + scope_access_thread_t state = { .scope = scope }; + sentry__waitable_flag_init(&state.started); + sentry__waitable_flag_init(&state.finished); + + TEST_ASSERT(sentry_scope_begin_read(scope) == 0); + sentry_value_t environment = sentry__scope_ref_environment(scope); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(environment), "old"); + sentry_value_decref(environment); + + sentry_threadid_t thread; + sentry__thread_init(&thread); + TEST_ASSERT( + !sentry__thread_spawn(&thread, write_scope_during_read, &state)); + TEST_ASSERT(sentry__waitable_flag_wait(&state.started, 1000)); + TEST_CHECK(!sentry__waitable_flag_wait(&state.finished, 10)); + + TEST_ASSERT(sentry_scope_begin_read(scope) == 0); + sentry_value_t tag + = scope_value_get_by_key(sentry__scope_load_tags, scope, "version"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(tag), "old"); + sentry_value_decref(tag); + TEST_CHECK(sentry_scope_begin_write(scope) == 1); + sentry_scope_end_read(scope); + TEST_CHECK(!sentry__waitable_flag_wait(&state.finished, 10)); + + sentry_scope_end_read(scope); + TEST_CHECK(sentry__waitable_flag_wait(&state.finished, 1000)); + sentry__thread_join(thread); + + TEST_CHECK_INT_EQUAL(state.result, 0); + environment = sentry__scope_ref_environment(scope); + tag = scope_value_get_by_key(sentry__scope_load_tags, scope, "version"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(environment), "new"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(tag), "new"); + sentry_value_decref(environment); + sentry_value_decref(tag); + sentry_scope_free(scope); +} + +SENTRY_TEST(scope_access_multiple) +{ + sentry_scope_t *first = sentry_scope_new(); + sentry_scope_t *second = sentry_scope_new(); + TEST_ASSERT(!!first && !!second); + + TEST_ASSERT(sentry_scope_begin_write(first) == 0); + TEST_ASSERT(sentry_scope_begin_read(second) == 0); + TEST_ASSERT(sentry_scope_begin_read(first) == 0); + TEST_ASSERT(sentry_scope_begin_write(first) == 0); + sentry_scope_set_tag(first, "key", "value"); + sentry_scope_end_write(first); + sentry_scope_end_read(first); + sentry_scope_end_write(first); + + TEST_CHECK(scope_value_get_length(sentry__scope_load_tags, second) == 0); + TEST_CHECK(sentry_scope_begin_write(second) == 1); + sentry_scope_end_read(second); + TEST_CHECK(scope_value_get_length(sentry__scope_load_tags, first) == 1); + + TEST_ASSERT(sentry_scope_begin_write(second) == 0); + sentry_scope_set_tag(second, "key", "value"); + sentry_scope_end_write(second); + TEST_CHECK(scope_value_get_length(sentry__scope_load_tags, second) == 1); + + sentry_scope_free(first); + sentry_scope_free(second); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 0397cc60b9..1dad9f1253 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -383,6 +383,7 @@ XX(rwlock_write_blocks_writer) XX(sampling_before_send) XX(sampling_decision) XX(sampling_transaction) +XX(scope_access_multiple) XX(scope_attachments) XX(scope_bind_span) XX(scope_bind_span_or_transaction_not_both) @@ -431,6 +432,8 @@ XX(scope_observer_transaction) XX(scope_observer_user) XX(scope_ownership) XX(scope_propagation_context) +XX(scope_read_blocks_write) +XX(scope_read_shared) XX(scope_rebind_same_object) XX(scope_release) XX(scope_remove_fingerprint_capture) @@ -443,6 +446,8 @@ XX(scope_transaction) XX(scope_update_context) XX(scope_user) XX(scope_user_id) +XX(scope_write_exclusive) +XX(scope_write_nested) XX(scoped_txn) XX(sentry__value_span_new_requires_unfinished_parent) XX(serialize_envelope) From 1f7b255c936a457f95d02077f8dea87870ffdc31 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 15 Oct 2026 15:06:31 +0200 Subject: [PATCH 11/23] feat(scope): Split global scope and expose isolation scope (#2160) Split the existing global scope into an internal, read-only global scope for SDK/OS metadata and configured defaults, and a mutable isolation scope used by top-level APIs. Move user data, observers, and backend flushing to the isolation scope. Add `sentry_scope_acquire()` and `sentry_scope_release()` for direct isolation scope access. Implement Sentry's three-scope model with global -> isolation -> local scope layering. Retain parent references and merge layers when preparing events, telemetry, and attachments. Local values take precedence over isolation and global defaults. The SDK follows the global mode used by other Sentry SDKs for client applications: all threads share the same process-wide isolation scope. Changes made through top-level APIs or an acquired reference are shared across all threads. See: - https://develop.sentry.dev/sdk/foundations/state-management/scopes/#isolation-scope --- CHANGELOG.md | 1 + include/sentry.h | 60 +- src/backends/sentry_backend_crashpad.cpp | 21 + src/backends/sentry_backend_native.c | 26 + src/integrations/sentry_integration_wer.c | 2 - src/sentry_core.c | 201 ++++-- src/sentry_core.h | 13 +- src/sentry_options.c | 2 + src/sentry_options.h | 2 + src/sentry_scope.c | 837 ++++++++++++++-------- src/sentry_scope.h | 48 +- src/sentry_session.c | 15 +- src/sentry_session.h | 2 +- tests/assertions.py | 7 +- tests/fixtures/integration.c | 19 + tests/test_integration_crashpad.py | 51 +- tests/test_integration_http.py | 34 + tests/test_integration_native.py | 33 + tests/unit/test_attachments.c | 16 +- tests/unit/test_concurrency.c | 12 +- tests/unit/test_logs.c | 9 + tests/unit/test_scope.c | 829 +++++++++++++++++++-- tests/unit/tests.inc | 10 + 23 files changed, 1739 insertions(+), 511 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e45a2f686..f1a66ea7a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ **Features**: - Add `sentry_scope_begin_read`/`sentry_scope_end_read` and `sentry_scope_begin_write`/`sentry_scope_end_write` for consistent reads of multiple scope properties and applying multiple scope changes with a single scope flush. ([#2107](https://github.com/getsentry/sentry-native/pull/2107)) +- Add `sentry_scope_acquire` and `sentry_scope_release` to access the isolation scope. ([#2160](https://github.com/getsentry/sentry-native/pull/2160)) ## Unreleased diff --git a/include/sentry.h b/include/sentry.h index f703b27ee0..d5b0e93eba 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1506,7 +1506,7 @@ SENTRY_API void sentry_options_free(sentry_options_t *opts); * The callback is invoked synchronously once during `sentry_init`, after * option-derived defaults are applied and before the crash backend is started. * The scope is borrowed. Configure it with `sentry_scope_*` functions rather - * than global scope functions such as `sentry_set_tag`. + * than top-level scope functions such as `sentry_set_tag`. */ typedef void (*sentry_initial_scope_function_t)( sentry_scope_t *scope, void *user_data); @@ -2613,6 +2613,33 @@ SENTRY_API sentry_user_consent_t sentry_user_consent_get(void); */ SENTRY_API int sentry_user_consent_is_required(void); +/** + * Acquires the isolation scope used by top-level scope functions. + * + * Returns an owned reference that must be released with `sentry_scope_release`. + * + * A separate isolation scope is used for each SDK run. The scope should be + * acquired after `sentry_init`, since scope changes made before initialization + * are not carried over. An explicitly acquired reference remains valid after + * the SDK is closed, but modifying the scope no longer has any effect. + * + * Note: The SDK operates in global mode: all threads share the same + * process-wide isolation scope, which inherits global data such as release and + * environment from the global scope. Changes made through top-level scope + * functions or the acquired scope are shared across all threads. + * + * For details on Sentry's scope model, see: + * https://develop.sentry.dev/sdk/foundations/state-management/scopes/#isolation-scope + */ +SENTRY_API sentry_scope_t *sentry_scope_acquire(void); + +/** + * Releases a reference acquired with `sentry_scope_acquire`. + * + * This is an alias for `sentry_scope_free`. + */ +SENTRY_API void sentry_scope_release(sentry_scope_t *scope); + /** * Creates a local scope. * @@ -2634,10 +2661,10 @@ SENTRY_API sentry_scope_t *sentry_local_scope_new(void); SENTRY_API sentry_scope_t *sentry_scope_new(void); /** - * Begins a batch read of `scope`. + * Begins a batch read of `scope` and its parent scopes. * * Tip: Individual scope property reads acquire a read lock automatically. - * Use this function to batch multiple reads under one read lock. + * Use this function to batch multiple reads. * * The scope remains read-locked until the matching `sentry_scope_end_read` call * on the same thread, so multiple properties can be read consistently. Other @@ -2673,9 +2700,10 @@ SENTRY_API void sentry_scope_end_read(const sentry_scope_t *scope); * write lock automatically. Use this function to batch multiple reads, writes, * or both under one write lock. * - * The scope remains write-locked until the matching - * `sentry_scope_end_write` call on the same thread. A write lock allows reads - * and writes. Writes may be nested for the same scope. + * The scope remains write-locked and its parent scopes read-locked until the + * matching `sentry_scope_end_write` call on the same thread, so inherited + * properties can be read consistently. A write lock allows reads and writes. + * Writes may be nested for the same scope. * * Note: A read lock cannot be upgraded to a write lock. If this thread already * holds a read lock on the scope, this function returns 1 without changing @@ -2691,7 +2719,7 @@ SENTRY_API int sentry_scope_begin_write(sentry_scope_t *scope); * Ends a write batch begun by `sentry_scope_begin_write`. * * The final matching call releases the scope write lock and flushes any - * pending global scope changes to the backend. This must be called on the + * pending isolation scope changes to the backend. This must be called on the * same thread as the matching begin call. */ SENTRY_API void sentry_scope_end_write(sentry_scope_t *scope); @@ -2719,7 +2747,7 @@ SENTRY_API sentry_scope_t *sentry_scope_clone(const sentry_scope_t *scope); SENTRY_API void sentry_scope_clear(sentry_scope_t *scope); /** - * Returns the ID of the last event sent with the global scope. + * Returns the ID of the last event sent with the isolation scope. * * Returns a nil UUID if no event has been sent. */ @@ -3323,13 +3351,13 @@ SENTRY_API log_return_value_t sentry_log( * Sends a structured log with a scope. * * Behaves like `sentry_log`, except the log also carries the attributes and - * trace of `scope`, layered on top of the global scope. An attribute set in - * more than one place resolves to the most specific: `attributes` > `scope` > - * global scope. + * trace of `scope`, layered on top of its isolation and global scopes. + * An attribute set in more than one place resolves to the most + * specific: `attributes` > `scope` > isolation scope > global scope. * * Scope ownership works as in `sentry_scope_capture_event`: a local scope * is freed by this function, a user-owned one is not. Pass `NULL` to apply the - * global scope only. + * isolation and global scopes. */ SENTRY_API log_return_value_t sentry_scope_capture_log(sentry_scope_t *scope, sentry_level_t level, const char *body, sentry_value_t attributes); @@ -3499,15 +3527,15 @@ typedef enum { * Records a metric of the given type with a scope. * * Behaves like the `sentry_metrics_*` functions, except the metric also carries - * the attributes and trace of `scope`, layered on top of the global scope. An - * attribute set in more than one place resolves to the most specific: - * `attributes` > `scope` > global scope. + * the attributes and trace of `scope`, layered on top of its isolation and + * global scopes. An attribute set in more than one place resolves to + * the most specific: `attributes` > `scope` > isolation scope > global scope. * * Ownership of `value` is transferred to this function, on top of `attributes`. * * Scope ownership works as in `sentry_scope_capture_event`: a local scope * is freed by this function, a user-owned one is not. Pass `NULL` to apply the - * global scope only. + * isolation and global scopes. */ SENTRY_API sentry_metrics_result_t sentry_scope_capture_metric( sentry_scope_t *scope, sentry_metric_type_t type, const char *name, diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 1f7967b0a3..d54740b003 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -325,9 +325,11 @@ flush_external_crash_report(crashpad_state_t *state, return; } sentry__envelope_set_event_id(envelope, crash_event_id); + sentry__mutex_lock((sentry_mutex_t *)&options->session_lock); if (options->session) { sentry__envelope_add_session(envelope, options->session); } + sentry__mutex_unlock((sentry_mutex_t *)&options->session_lock); if (options->cache_keep) { sentry__envelope_set_header(envelope, "cache_dir", @@ -954,6 +956,24 @@ remove_attachment(void *state, sentry_value_t attachment) } #endif +static void +clear_scope(void *state) +{ + auto *data = static_cast(state); + if (!data) { + return; + } + + const base::FilePath *paths[] + = { &data->breadcrumb1_path, &data->breadcrumb2_path }; + for (const auto *path : paths) { + if (!path->empty() && write_attachment(data, *path, "", 0) != 0) { + SENTRY_WARN("failed to clear persisted breadcrumbs"); + } + } + data->num_breadcrumbs = 0; +} + static int crashpad_backend_startup( sentry_backend_t *backend, const sentry_options_t *options) @@ -1201,6 +1221,7 @@ crashpad_backend_startup( sentry_scope_observer_t *observer = sentry__scope_observer_new(); if (observer) { observer->data = data; + observer->clear = clear_scope; observer->add_breadcrumb = crashpad_backend_add_breadcrumb; #if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX) \ || defined(SENTRY_PLATFORM_MACOS) diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index d39f51a8be..dcea80d18c 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -602,6 +602,31 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, #endif } +static void +clear_scope(void *data) +{ + native_backend_state_t *state = (native_backend_state_t *)data; + if (!state) { + return; + } + + const sentry_path_t *paths[] + = { state->breadcrumb1_path, state->breadcrumb2_path }; + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) { + if (paths[i] && sentry__path_write_buffer(paths[i], "", 0) != 0) { + SENTRY_WARN("failed to clear persisted breadcrumbs"); + } + } + state->num_breadcrumbs = 0; + + sentry_path_t *path + = sentry__path_join_str(state->run_path, "__sentry-attachments"); + if (path && sentry__path_is_file(path) && sentry__path_remove(path) != 0) { + SENTRY_WARN("failed to clear attachment manifest"); + } + sentry__path_free(path); +} + static int native_backend_startup( sentry_backend_t *backend, const sentry_options_t *options) @@ -1013,6 +1038,7 @@ native_backend_startup( sentry_scope_observer_t *observer = sentry__scope_observer_new(); if (observer) { observer->data = state; + observer->clear = clear_scope; observer->add_breadcrumb = native_backend_add_breadcrumb; observer->add_attachment = add_attachment; SENTRY_WITH_SCOPE_MUT (scope) { diff --git a/src/integrations/sentry_integration_wer.c b/src/integrations/sentry_integration_wer.c index f59e14c8fc..e7156fb9fe 100644 --- a/src/integrations/sentry_integration_wer.c +++ b/src/integrations/sentry_integration_wer.c @@ -232,8 +232,6 @@ wer_clear(void *data) sentry_value_t tags = sentry__scope_load_tags(scope); sentry_value_foreach_key_value(tags, wer_cleanup_tag, wer_data); sentry_value_decref(tags); - - wer_for_each_attachment(scope, wer_data, wer_remove_attachment); } static void diff --git a/src/sentry_core.c b/src/sentry_core.c index ba2c1527e7..7e9338809b 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -36,6 +36,7 @@ #endif static sentry_options_t *g_options = NULL; +static sentry_scope_observer_t *g_scope_observer = NULL; #ifdef SENTRY__MUTEX_INIT_DYN SENTRY__MUTEX_INIT_DYN(g_options_lock) #else @@ -81,7 +82,7 @@ sentry__should_skip_upload(void) } static void -flush_global_scope(sentry_scope_t *scope) +flush_scope(sentry_scope_t *scope) { sentry_options_t *options = sentry__options_incref(scope->state); if (options && options->backend && options->backend->flush_scope_func) { @@ -90,6 +91,65 @@ flush_global_scope(sentry_scope_t *scope) sentry_options_free(options); } +static void +update_session_user(void *data, sentry_value_t user) +{ + sentry_scope_t *scope = data; + sentry_options_t *options = sentry__options_incref(scope->state); + sentry__mutex_lock(&options->session_lock); + if (options->session) { + sentry__session_sync_user(options->session, user, + options->run ? options->run->installation_id : NULL); + sentry__run_write_session(options->run, options->session); + sentry__session_notify_changed(options); + } + sentry__mutex_unlock(&options->session_lock); + sentry_options_free(options); +} + +static void +clear_session_user(void *data) +{ + update_session_user(data, sentry_value_new_null()); +} + +static bool +register_scope_observer(sentry_scope_t *scope, sentry_options_t *options) +{ + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + if (!observer) { + return false; + } + observer->data = scope; + observer->clear = clear_session_user; + observer->set_user = update_session_user; + sentry__mutex_lock(&scope->observers_lock); + if (!sentry__scope_add_observer(scope, observer)) { + sentry__mutex_unlock(&scope->observers_lock); + return false; + } + sentry__scope_set_state(scope, sentry__options_incref(options)); + g_scope_observer = observer; + sentry__mutex_unlock(&scope->observers_lock); + return true; +} + +static void +unregister_scope_observer(sentry_scope_t *scope) +{ + if (!g_scope_observer) { + return; + } + sentry__mutex_lock(&scope->observers_lock); + sentry_options_t *options = scope->state; + sentry__scope_set_flush_func(scope, NULL); + sentry__scope_remove_observer(scope, g_scope_observer); + g_scope_observer = NULL; + sentry__scope_set_state(scope, NULL); + sentry__mutex_unlock(&scope->observers_lock); + sentry_options_free(options); +} + static void register_integrations(sentry_scope_t *scope, const sentry_options_t *options) { @@ -161,6 +221,12 @@ sentry_init(sentry_options_t *options) sentry_close(); + if (!sentry__scope_init(options)) { + SENTRY_WARN("failed to initialize scopes"); + goto fail; + } + initial_scope_prepared = true; + sentry__client_report_reset(); sentry_logger_t logger = { NULL, NULL, SENTRY_LEVEL_DEBUG }; @@ -242,9 +308,14 @@ sentry_init(sentry_options_t *options) #endif SENTRY_WITH_SCOPE_MUT (scope) { - sentry__scope_apply_options(scope, options); + if (register_scope_observer(scope, options)) { + sentry__scope_apply_options(scope, options); + } + } + if (!g_scope_observer) { + SENTRY_WARN("failed to register scope observer"); + goto fail; } - initial_scope_prepared = true; // and then we will start the backend, since it requires a valid run sentry_backend_t *backend = options->backend; @@ -268,8 +339,7 @@ sentry_init(sentry_options_t *options) // *after* setting the global options, register integrations and trigger a // scope and consent flush, since at least crashpad needs that. SENTRY_WITH_SCOPE_MUT (scope) { - sentry__scope_set_state(scope, sentry__options_incref(options)); - sentry__scope_set_flush_func(scope, flush_global_scope); + sentry__scope_set_flush_func(scope, flush_scope); register_integrations(scope, options); sentry__scope_flush(scope); } @@ -306,6 +376,11 @@ sentry_init(sentry_options_t *options) fail: SENTRY_WARN("`sentry_init` failed"); + if (initial_scope_prepared) { + SENTRY_WITH_SCOPE_MUT (scope) { + unregister_scope_observer(scope); + } + } if (transport) { sentry__transport_shutdown(transport, 0); } @@ -367,10 +442,7 @@ sentry_close(void) size_t dumped_envelopes = 0; if (options) { SENTRY_WITH_SCOPE_MUT (scope) { - sentry_options_t *scope_options = scope->state; - sentry__scope_set_flush_func(scope, NULL); - sentry__scope_set_state(scope, NULL); - sentry_options_free(scope_options); + unregister_scope_observer(scope); } sentry_end_session(); if (options->backend && options->backend->shutdown_func) { @@ -417,7 +489,11 @@ sentry_reinstall_backend(void) int rv = 0; SENTRY_WITH_OPTIONS (options) { // prevent scope observers from racing with backend reinstall - sentry_scope_t *scope = sentry__scope_getref(); + sentry_scope_t *scope = sentry_scope_acquire(); + if (!scope) { + rv = 1; + continue; + } sentry__mutex_lock(&scope->observers_lock); sentry_backend_t *backend = options->backend; if (backend) { @@ -439,7 +515,7 @@ sentry_reinstall_backend(void) } } sentry__mutex_unlock(&scope->observers_lock); - sentry__scope_finish(scope); + sentry_scope_release(scope); } return rv; } @@ -671,7 +747,8 @@ sentry__capture_event( bool has_session = options->session; SENTRY_TSAN_IGNORE_READS_END(); if (has_session) { - sentry_options_t *mut_options = sentry__options_lock(); + sentry_options_t *mut_options = (sentry_options_t *)options; + sentry__mutex_lock(&mut_options->session_lock); // recheck inside the lock since our previous read is racy. if (mut_options->session) { sentry__envelope_add_session( @@ -683,7 +760,7 @@ sentry__capture_event( mut_options->session->init = false; sentry__session_notify_changed(mut_options); } - sentry__options_unlock(); + sentry__mutex_unlock(&mut_options->session_lock); } bool should_skip = !sentry__roll_dice(options->sample_rate); @@ -761,17 +838,12 @@ prepare_attachments(sentry_hint_t *hint, sentry_scope_t *local_scope) if (hint) { sentry__attachments_extend(&attachments, hint->attachments); } - if (local_scope) { - sentry_value_t local_attachments - = sentry__scope_load_attachments(local_scope); - sentry__attachments_extend(&attachments, local_attachments); - sentry_value_decref(local_attachments); - } - SENTRY_WITH_SCOPE (scope) { - sentry_value_t global_attachments - = sentry__scope_load_attachments(scope); - sentry__attachments_extend(&attachments, global_attachments); - sentry_value_decref(global_attachments); + sentry_scope_t *scope = local_scope ? local_scope : sentry_scope_acquire(); + sentry_value_t scope_attachments = sentry__scope_load_attachments(scope); + sentry__attachments_extend(&attachments, scope_attachments); + sentry_value_decref(scope_attachments); + if (!local_scope) { + sentry_scope_release(scope); } if (hint) { sentry__hint_set_attachments(hint, sentry_value_incref(attachments)); @@ -807,6 +879,23 @@ sentry__invoke_before_send( return event; } +sentry_value_t +sentry__invoke_before_breadcrumb( + const sentry_options_t *options, sentry_value_t breadcrumb) +{ + if (!options || !options->before_breadcrumb_func) { + return breadcrumb; + } + SENTRY_DEBUG("invoking `before_breadcrumb` hook"); + breadcrumb = options->before_breadcrumb_func( + breadcrumb, options->before_breadcrumb_data); + if (sentry_value_is_null(breadcrumb)) { + SENTRY_DEBUG( + "breadcrumb was discarded by the `before_breadcrumb` hook"); + } + return breadcrumb; +} + sentry_value_t sentry__prepare_event(const sentry_options_t *options, sentry_value_t event, sentry_scope_t *local_scope) @@ -815,19 +904,16 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event, sentry__record_errors_on_current_session(1); } + sentry_scope_mode_t mode = SENTRY_SCOPE_ALL; + if (!options->symbolize_stacktraces) { + mode &= ~SENTRY_SCOPE_STACKTRACES; + } if (local_scope) { - SENTRY_DEBUG("merging local scope into event"); - sentry_scope_mode_t mode = SENTRY_SCOPE_BREADCRUMBS; sentry__scope_apply_to_event(local_scope, options, event, mode); - } - - SENTRY_WITH_SCOPE (scope) { - SENTRY_DEBUG("merging global scope into event"); - sentry_scope_mode_t mode = SENTRY_SCOPE_ALL; - if (!options->symbolize_stacktraces) { - mode &= ~SENTRY_SCOPE_STACKTRACES; + } else { + SENTRY_WITH_SCOPE (scope) { + sentry__scope_apply_to_event(scope, options, event, mode); } - sentry__scope_apply_to_event(scope, options, event, mode); } return event; } @@ -937,13 +1023,13 @@ prepare_user_feedback(const sentry_options_t *options, event, "level", sentry__value_new_level(SENTRY_LEVEL_INFO)); if (local_scope) { - SENTRY_DEBUG("merging local scope into feedback event"); sentry__scope_apply_to_event( local_scope, options, event, SENTRY_SCOPE_NONE); - } - SENTRY_WITH_SCOPE (scope) { - SENTRY_DEBUG("merging global scope into feedback event"); - sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_NONE); + } else { + SENTRY_WITH_SCOPE (scope) { + sentry__scope_apply_to_event( + scope, options, event, SENTRY_SCOPE_NONE); + } } sentry_value_t all_attachments = prepare_attachments(hint, local_scope); @@ -1087,17 +1173,6 @@ sentry__ensure_event_id(sentry_value_t event, sentry_uuid_t *uuid_out) void sentry_set_user(sentry_value_t user) { - if (!sentry_value_is_null(user)) { - sentry_options_t *options = sentry__options_lock(); - if (options && options->session) { - sentry__session_sync_user(options->session, user, - options->run ? options->run->installation_id : NULL); - sentry__run_write_session(options->run, options->session); - sentry__session_notify_changed(options); - } - sentry__options_unlock(); - } - SENTRY_WITH_SCOPE_MUT (scope) { sentry_scope_set_user(scope, user); } @@ -1140,24 +1215,6 @@ sentry_set_environment(const char *environment) void sentry_add_breadcrumb(sentry_value_t breadcrumb) { - bool discarded = false; - SENTRY_WITH_OPTIONS (options) { - if (options->before_breadcrumb_func) { - SENTRY_DEBUG("invoking `before_breadcrumb` hook"); - breadcrumb = options->before_breadcrumb_func( - breadcrumb, options->before_breadcrumb_data); - if (sentry_value_is_null(breadcrumb)) { - SENTRY_DEBUG("breadcrumb was discarded by the " - "`before_breadcrumb` hook"); - discarded = true; - } - } - } - - if (discarded) { - return; - } - // backend observers persist breadcrumbs without a scope flush SENTRY_WITH_SCOPE_MUT (scope) { sentry_scope_add_breadcrumb(scope, breadcrumb); @@ -1317,9 +1374,11 @@ sentry__apply_to_telemetry(const sentry_scope_t *scope, { if (scope) { sentry__scope_apply_to_telemetry(scope, telemetry, attributes); - } - SENTRY_WITH_SCOPE (global_scope) { - sentry__scope_apply_to_telemetry(global_scope, telemetry, attributes); + } else { + SENTRY_WITH_SCOPE (global_scope) { + sentry__scope_apply_to_telemetry( + global_scope, telemetry, attributes); + } } sentry__value_add_attribute(attributes, sentry_value_new_string(sentry_options_get_sdk_name(options)), "string", diff --git a/src/sentry_core.h b/src/sentry_core.h index b3aa24c027..5b3141c09f 100644 --- a/src/sentry_core.h +++ b/src/sentry_core.h @@ -65,9 +65,15 @@ sentry_value_t sentry__invoke_on_crash(const sentry_options_t *options, sentry_value_t sentry__invoke_before_send( const sentry_options_t *options, sentry_value_t event, sentry_hint_t *hint); +/** + * Invokes the configured `before_breadcrumb` callback, if any. + */ +sentry_value_t sentry__invoke_before_breadcrumb( + const sentry_options_t *options, sentry_value_t breadcrumb); + /** * Prepares an event by recording errors on the current session and applying - * the local and global scopes. + * the local, isolation, and global scopes. * * Returns `event` without transferring ownership. */ @@ -123,7 +129,7 @@ void sentry__submit_envelope(sentry_transport_t *transport, sentry_envelope_t *envelope, const sentry_options_t *options); /** - * Captures the `envelope` on the global scope, recording the last sent event + * Captures the `envelope` on the isolation scope, recording the last sent event * ID. * * Note: This is not safe to call from crash handlers; use @@ -166,7 +172,8 @@ void sentry__set_propagation_context(const char *key, sentry_value_t value); * Populates a telemetry item (a log or metric) with attributes and scope data * from all available sources. Values are applied in precedence order, where the * first write wins: per-call attributes already present in `attributes`, then - * the passed `scope`, then the global scope, and finally the options. + * the passed `scope`, then its isolation and global scopes, and finally the + * options. */ void sentry__apply_to_telemetry(const sentry_scope_t *scope, const sentry_options_t *options, sentry_value_t telemetry, diff --git a/src/sentry_options.c b/src/sentry_options.c index 54c01d0512..4d8a0e9131 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -41,6 +41,7 @@ sentry_options_new(void) if (!opts) { return NULL; } + sentry__mutex_init(&opts->session_lock); opts->database_path = sentry__path_from_str(".sentry-native"); // we assume the DSN to be ASCII only sentry_options_set_dsn(opts, getenv("SENTRY_DSN")); @@ -197,6 +198,7 @@ sentry_options_free(sentry_options_t *opts) } sentry_free(opts->integrations); + sentry__mutex_free(&opts->session_lock); sentry_free(opts); } diff --git a/src/sentry_options.h b/src/sentry_options.h index e3cb1b17bd..d9ab9a3924 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -8,6 +8,7 @@ #include "sentry_integration.h" #include "sentry_logger.h" #include "sentry_session.h" +#include "sentry_sync.h" #include "sentry_utils.h" // Defaults to 2s as per @@ -101,6 +102,7 @@ struct sentry_options_s { not exposed through the options API */ struct sentry_backend_s *backend; sentry_session_t *session; + sentry_mutex_t session_lock; sentry_integration_t **integrations; size_t num_integrations; bool retain_crash_marker; diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 638a01e8ae..41b49b8612 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -28,6 +28,8 @@ # define SENTRY_BACKEND "native" #endif +static const sentry_level_t SENTRY_LEVEL_NONE = SENTRY_LEVEL_TRACE - 1; + struct sentry_scope_data_s { sentry_rwlock_t rwlock; @@ -65,20 +67,28 @@ typedef struct scope_access_s { sentry_scope_data_t *data; size_t depth; bool write; + bool pooled; struct scope_access_s *next; } scope_access_t; -static bool g_scope_initialized = false; -static sentry_scope_t g_scope = { 0 }; +static long g_scope_initialized = 0; static sentry_scope_data_t g_scope_data = { 0 }; -static bool g_scope_idle_initialized = false; -static sentry_cond_t g_scope_idle; +static sentry_scope_t g_scope = { .refcount = -1, .data = &g_scope_data }; +static sentry_scope_t *g_isolation_scope = NULL; +// keep common scope access off the heap during capture: +// 1. global scope +// 2. isolation scope +// 3. local or user scope +// 4. a nested user scope, e.g. a clone batched inside its source's batch +// deeper nesting falls back to the heap +#define SCOPE_ACCESS_POOL_SIZE 4 #ifdef _MSC_VER -static __declspec(thread) size_t g_scope_depth; static __declspec(thread) scope_access_t *g_scope_access; +static __declspec(thread) scope_access_t + g_scope_access_pool[SCOPE_ACCESS_POOL_SIZE]; #else -static __thread size_t g_scope_depth; static __thread scope_access_t *g_scope_access; +static __thread scope_access_t g_scope_access_pool[SCOPE_ACCESS_POOL_SIZE]; #endif #ifdef SENTRY__MUTEX_INIT_DYN SENTRY__MUTEX_INIT_DYN(g_lock) @@ -218,7 +228,7 @@ get_client_sdk(void) } static void -init_scope_data(sentry_scope_data_t *data) +init_scope_data(sentry_scope_data_t *data, size_t max_breadcrumbs) { data->release = sentry_value_new_null(); data->environment = sentry_value_new_null(); @@ -230,9 +240,9 @@ init_scope_data(sentry_scope_data_t *data) data->attributes = sentry_value_new_object(); data->contexts = sentry_value_new_object(); data->propagation_context = sentry_value_new_object(); - data->breadcrumbs = sentry__ringbuffer_new(SENTRY_BREADCRUMBS_MAX); + data->breadcrumbs = sentry__ringbuffer_new(max_breadcrumbs); data->dynamic_sampling_context = sentry_value_new_object(); - data->level = SENTRY_LEVEL_ERROR; + data->level = SENTRY_LEVEL_NONE; data->last_event_id = sentry_uuid_nil(); data->client_sdk = sentry_value_new_null(); data->attachments = sentry_value_new_list(); @@ -268,7 +278,7 @@ new_scope_data(void) sentry_scope_data_t *data = SENTRY_MAKE(sentry_scope_data_t); if (data) { sentry__rwlock_init(&data->rwlock); - init_scope_data(data); + init_scope_data(data, SENTRY_BREADCRUMBS_MAX); } return data; } @@ -284,29 +294,6 @@ free_scope_data(sentry_scope_data_t *data) sentry_free(data); } -static void -cleanup_global_data(sentry_scope_data_t *data) -{ - cleanup_scope_data(data); - sentry__rwlock_free(&data->rwlock); -} - -static void -init_global_data(sentry_scope_data_t *data) -{ - sentry__value_replace(&data->user, sentry_value_new_object()); - sentry_value_set_by_key(data->contexts, "os", sentry__get_os_context()); -#if defined(SENTRY_PLATFORM_WINDOWS) && !defined(SENTRY_PLATFORM_XBOX) - sentry_value_t wine_context = sentry__get_wine_context(); - if (!sentry_value_is_null(wine_context)) { - sentry_value_set_by_key(data->contexts, "wine", wine_context); - } else { - sentry_value_decref(wine_context); - } -#endif - sentry__value_replace(&data->client_sdk, get_client_sdk()); -} - static void clear_scope_data(sentry_scope_data_t *data) { @@ -319,8 +306,10 @@ clear_scope_data(sentry_scope_data_t *data) sentry_value_t dynamic_sampling_context = sentry_value_incref(data->dynamic_sampling_context); + size_t max_breadcrumbs = data->breadcrumbs ? data->breadcrumbs->max_size + : SENTRY_BREADCRUMBS_MAX; cleanup_scope_data(data); - init_scope_data(data); + init_scope_data(data, max_breadcrumbs); sentry_value_decref(data->propagation_context); sentry_value_decref(data->dynamic_sampling_context); @@ -409,18 +398,22 @@ sentry__scope_update_dsc(sentry_scope_t *scope, const sentry_options_t *options) dsc, "sample_rate", sentry_value_new_double(1.0)); } + sentry_value_t parent_trace = scope->parent + ? sentry__scope_load_trace_context(scope->parent) + : sentry_value_new_null(); SENTRY_SCOPE_WRITE_LOCK (data) { - sentry_value_t sample_rand = sentry_value_get_by_key( - sentry_value_get_by_key(data->propagation_context, "trace"), - "sample_rand"); - sentry_value_set_by_key( - dsc, "sample_rand", sentry_value_incref(sample_rand)); - sentry_value_set_by_key( - dsc, "release", sentry_value_incref(data->release)); - sentry_value_set_by_key( - dsc, "environment", sentry_value_incref(data->environment)); + sentry_value_t trace + = sentry_value_get_by_key(data->propagation_context, "trace"); + sentry_value_t sample_rand + = sentry_value_get_by_key_owned(trace, "sample_rand"); + if (sentry_value_is_null(trace) && scope->parent) { + sample_rand + = sentry_value_get_by_key_owned(parent_trace, "sample_rand"); + } + sentry_value_set_by_key(dsc, "sample_rand", sample_rand); sentry__value_replace(&data->dynamic_sampling_context, dsc); } + sentry_value_decref(parent_trace); } static bool @@ -445,31 +438,147 @@ init_scope(sentry_scope_t *scope, sentry_scope_data_t *data) scope->pending_flush = false; scope->flush_func = NULL; scope->state = NULL; + scope->parent = NULL; sentry__mutex_init(&scope->observers_lock); scope->one_shot = false; return true; } -static sentry_scope_t * -get_scope(void) +static const sentry_scope_t * +get_global_scope(void) { - // cleanup clears g_scope_initialized before existing references finish - if (g_scope.data) { + if (sentry__atomic_fetch(&g_scope_initialized)) { return &g_scope; } +#ifndef SENTRY_PLATFORM_WINDOWS + if (!sentry__block_for_signal_handler()) { + return NULL; + } +#endif + SENTRY__MUTEX_INIT_DYN_ONCE(g_lock); + sentry__mutex_lock(&g_lock); + if (!sentry__atomic_fetch(&g_scope_initialized)) { + sentry_scope_data_t *data = &g_scope_data; + sentry__rwlock_init(&data->rwlock); + init_scope_data(data, 0); + data->level = SENTRY_LEVEL_ERROR; + data->trace_managed = false; + sentry_value_t contexts = data->contexts; + sentry_value_t sdk = get_client_sdk(); + if (sentry_value_is_null(contexts) || sentry_value_is_null(sdk)) { + cleanup_scope_data(data); + sentry__rwlock_free(&data->rwlock); + sentry_value_decref(sdk); + sentry__mutex_unlock(&g_lock); + return NULL; + } + sentry_value_set_by_key(contexts, "os", sentry__get_os_context()); +#if defined(SENTRY_PLATFORM_WINDOWS) && !defined(SENTRY_PLATFORM_XBOX) + sentry_value_t wine_context = sentry__get_wine_context(); + if (!sentry_value_is_null(wine_context)) { + sentry_value_set_by_key(contexts, "wine", wine_context); + } else { + sentry_value_decref(wine_context); + } +#endif + sentry_value_freeze(contexts); + sentry_value_freeze(sdk); + sentry__value_replace(&data->client_sdk, sdk); + sentry__atomic_store(&g_scope_initialized, 1); + } + sentry__mutex_unlock(&g_lock); + return &g_scope; +} - memset(&g_scope, 0, sizeof(sentry_scope_t)); - memset(&g_scope_data, 0, sizeof(sentry_scope_data_t)); - sentry__rwlock_init(&g_scope_data.rwlock); - init_scope_data(&g_scope_data); - if (!init_scope(&g_scope, &g_scope_data)) { - return &g_scope; +static bool +init_global_scope(const sentry_options_t *options) +{ + const sentry_scope_t *scope = get_global_scope(); + if (!scope) { + return false; + } + sentry_value_t sdk = get_client_sdk(); + if (sentry_value_is_null(sdk)) { + return false; + } + if (options->sdk_name) { + sentry_value_set_by_key( + sdk, "name", sentry_value_new_string(options->sdk_name)); + } + sentry_value_t integrations = sentry_value_get_by_key(sdk, "integrations"); + for (size_t i = 0; i < options->num_integrations; i++) { + const char *name = options->integrations[i]->name; + if (!name) { + continue; + } + if (sentry_value_is_null(integrations)) { + integrations = sentry_value_new_list(); + sentry_value_set_by_key(sdk, "integrations", integrations); + } + sentry_value_append(integrations, sentry_value_new_string(name)); + } + sentry_value_freeze(sdk); + sentry_value_t attachments + = sentry__attachments_clone(options->attachments); + sentry_value_freeze(attachments); + SENTRY_SCOPE_WRITE_LOCK (scope->data) { + sentry__value_replace(&scope->data->client_sdk, sdk); + sentry__value_replace(&scope->data->attachments, attachments); + sentry__value_replace( + &scope->data->release, sentry_value_new_string(options->release)); + sentry__value_replace(&scope->data->environment, + sentry_value_new_string(options->environment)); } - init_global_data(g_scope.data); + return true; +} - g_scope_initialized = true; +static sentry_scope_t * +alloc_scope(void) +{ + sentry_scope_t *scope = SENTRY_MAKE(sentry_scope_t); + if (!scope) { + return NULL; + } - return &g_scope; + if (!init_scope(scope, NULL)) { + sentry_free(scope); + return NULL; + } + return scope; +} + +static sentry_scope_t * +new_isolation_scope(void) +{ + const sentry_scope_t *global = get_global_scope(); + if (!global) { + return NULL; + } + sentry_scope_t *scope = alloc_scope(); + if (scope) { + scope->parent = sentry__scope_incref(global); + } + return scope; +} + +bool +sentry__scope_init(const sentry_options_t *options) +{ + if (!init_global_scope(options)) { + return false; + } + sentry_scope_t *scope = new_isolation_scope(); + if (!scope) { + return false; + } + + SENTRY__MUTEX_INIT_DYN_ONCE(g_lock); + sentry__mutex_lock(&g_lock); + sentry_scope_t *old_scope = g_isolation_scope; + g_isolation_scope = scope; + sentry__mutex_unlock(&g_lock); + sentry__scope_decref(old_scope); + return true; } static void @@ -492,33 +601,22 @@ cleanup_scope(sentry_scope_t *scope) scope->data = NULL; cleanup_observers(scope); sentry__mutex_free(&scope->observers_lock); + sentry__scope_decref((sentry_scope_t *)scope->parent); } sentry_scope_t * -sentry__scope_incref(sentry_scope_t *scope) +sentry__scope_incref(const sentry_scope_t *scope) { - if (scope) { - sentry__atomic_fetch_and_add(&scope->refcount, 1); + if (scope && sentry__atomic_fetch((long *)&scope->refcount) >= 0) { + sentry__atomic_fetch_and_add((long *)&scope->refcount, 1); } - return scope; + return (sentry_scope_t *)scope; } void sentry__scope_decref(sentry_scope_t *scope) { - if (!scope) { - return; - } - - if (scope == &g_scope) { - SENTRY__MUTEX_INIT_DYN_ONCE(g_lock); - sentry__mutex_lock(&g_lock); - long refcount = sentry__atomic_fetch_and_add(&scope->refcount, -1); - assert(refcount > 1); - if (refcount == 2 && g_scope_idle_initialized) { - sentry__cond_wake_all(&g_scope_idle); - } - sentry__mutex_unlock(&g_lock); + if (!scope || sentry__atomic_fetch(&scope->refcount) < 0) { return; } @@ -540,66 +638,31 @@ sentry__scope_cleanup(void) SENTRY__MUTEX_INIT_DYN_ONCE(g_lock); sentry__mutex_lock(&g_lock); sentry__options_unlock(); - if (!g_scope_idle_initialized) { - sentry__cond_init(&g_scope_idle); - g_scope_idle_initialized = true; - } - if (g_scope_initialized) { - g_scope_initialized = false; - while (sentry__atomic_fetch(&g_scope.refcount) > 1) { - sentry__cond_wait(&g_scope_idle, &g_lock); - } - cleanup_global_data(g_scope.data); - g_scope.data = NULL; - cleanup_observers(&g_scope); - sentry__mutex_free(&g_scope.observers_lock); - } - sentry__cond_wake_all(&g_scope_idle); + sentry_scope_t *scope = g_isolation_scope; + g_isolation_scope = NULL; sentry__mutex_unlock(&g_lock); + sentry__scope_decref(scope); } sentry_scope_t * -sentry__scope_getref(void) +sentry_scope_acquire(void) { #ifndef SENTRY_PLATFORM_WINDOWS - // avoid dynamic TLS access in the signal handler + // avoid dynamic mutex initialization in the signal handler if (!sentry__block_for_signal_handler()) { - return sentry__scope_incref(get_scope()); + return sentry__scope_incref(g_isolation_scope); } #endif SENTRY__MUTEX_INIT_DYN_ONCE(g_lock); sentry__mutex_lock(&g_lock); - if (!g_scope_idle_initialized) { - sentry__cond_init(&g_scope_idle); - g_scope_idle_initialized = true; + if (!g_isolation_scope) { + g_isolation_scope = new_isolation_scope(); } - // let existing callers finish reentrant scope access during cleanup - while (!g_scope_initialized && g_scope.data && !g_scope_depth) { - sentry__cond_wait(&g_scope_idle, &g_lock); - } - sentry_scope_t *scope = sentry__scope_incref(get_scope()); - g_scope_depth++; + sentry_scope_t *scope = sentry__scope_incref(g_isolation_scope); sentry__mutex_unlock(&g_lock); return scope; } -void -sentry__scope_finish(sentry_scope_t *scope) -{ - if (!scope) { - return; - } - - if (scope == &g_scope -#ifndef SENTRY_PLATFORM_WINDOWS - && sentry__block_for_signal_handler() -#endif - ) { - g_scope_depth--; - } - sentry__scope_decref(scope); -} - static void flush_scope(sentry_scope_t *scope) { @@ -619,13 +682,6 @@ flush_scope(sentry_scope_t *scope) sentry__mutex_unlock(&scope->observers_lock); } -void -sentry__scope_finish_mut(sentry_scope_t *scope) -{ - flush_scope(scope); - sentry__scope_finish(scope); -} - void sentry__scope_set_flush_func( sentry_scope_t *scope, void (*func)(sentry_scope_t *scope)) @@ -770,13 +826,13 @@ end_scope_notify(sentry_scope_t *scope) sentry_scope_t * sentry_scope_new(void) { - sentry_scope_t *scope = SENTRY_MAKE(sentry_scope_t); + sentry_scope_t *scope = alloc_scope(); if (!scope) { return NULL; } - - if (!init_scope(scope, NULL)) { - sentry_free(scope); + scope->parent = sentry_scope_acquire(); + if (!scope->parent) { + sentry_scope_free(scope); return NULL; } return scope; @@ -804,7 +860,17 @@ scope_begin(const sentry_scope_t *scope, bool write) return 0; } - access = SENTRY_MAKE(scope_access_t); + // depth == 0 marks a free slot + for (size_t i = 0; i < SCOPE_ACCESS_POOL_SIZE; i++) { + if (g_scope_access_pool[i].depth == 0) { + access = &g_scope_access_pool[i]; + access->pooled = true; + break; + } + } + if (!access) { + access = SENTRY_MAKE(scope_access_t); + } if (!access) { return 1; } @@ -826,13 +892,37 @@ scope_begin(const sentry_scope_t *scope, bool write) int sentry_scope_begin_read(const sentry_scope_t *scope) { - return scope_begin(scope, false); + if (!scope) { + return 1; + } + if (scope->parent && sentry_scope_begin_read(scope->parent)) { + return 1; + } + if (!scope_begin(scope, false)) { + return 0; + } + if (scope->parent) { + sentry_scope_end_read(scope->parent); + } + return 1; } int sentry_scope_begin_write(sentry_scope_t *scope) { - return scope_begin(scope, true); + if (!scope) { + return 1; + } + if (scope->parent && sentry_scope_begin_read(scope->parent)) { + return 1; + } + if (!scope_begin(scope, true)) { + return 0; + } + if (scope->parent) { + sentry_scope_end_read(scope->parent); + } + return 1; } static void @@ -862,7 +952,9 @@ scope_end(const sentry_scope_t *scope) *link = access->next; bool write = access->write; - sentry_free(access); + if (!access->pooled) { + sentry_free(access); + } if (!write) { sentry__rwlock_read_unlock(&scope->data->rwlock); return; @@ -876,21 +968,34 @@ scope_end(const sentry_scope_t *scope) void sentry_scope_end_read(const sentry_scope_t *scope) { + if (!scope) { + return; + } scope_end(scope); + if (scope->parent) { + sentry_scope_end_read(scope->parent); + } } void sentry_scope_end_write(sentry_scope_t *scope) { - scope_end(scope); + sentry_scope_end_read(scope); } void sentry_scope_free(sentry_scope_t *scope) { + flush_scope(scope); sentry__scope_decref(scope); } +void +sentry_scope_release(sentry_scope_t *scope) +{ + sentry_scope_free(scope); +} + bool sentry__scope_is_one_shot(const sentry_scope_t *scope) { @@ -929,37 +1034,10 @@ sentry__scope_apply_options(sentry_scope_t *scope, sentry_options_t *options) { sentry_scope_data_t *data = scope->data; SENTRY_SCOPE_WRITE_LOCK (data) { - if (options->sdk_name) { - sentry_value_t sdk_name - = sentry_value_new_string(options->sdk_name); - sentry_value_set_by_key(data->client_sdk, "name", sdk_name); - } - sentry_value_t integrations - = sentry_value_get_by_key(data->client_sdk, "integrations"); - for (size_t i = 0; i < options->num_integrations; i++) { - const char *name = options->integrations[i]->name; - if (!name) { - continue; - } - if (sentry_value_is_null(integrations)) { - integrations = sentry_value_new_list(); - sentry_value_set_by_key( - data->client_sdk, "integrations", integrations); - } - sentry_value_append(integrations, sentry_value_new_string(name)); - } - sentry_value_freeze(data->client_sdk); generate_propagation_context(data->propagation_context); - sentry_value_decref(data->attachments); - data->attachments = options->attachments; - options->attachments = sentry_value_new_null(); sentry__ringbuffer_set_max_size( data->breadcrumbs, options->max_breadcrumbs); } - sentry_scope_set_release_n( - scope, options->release, sentry__guarded_strlen(options->release)); - sentry_scope_set_environment_n(scope, options->environment, - sentry__guarded_strlen(options->environment)); if (options->initial_scope_func) { options->initial_scope_func(scope, options->initial_scope_data); @@ -982,9 +1060,10 @@ sentry_scope_clear(sentry_scope_t *scope) observer->clear(observer->data); } } - end_scope_notify(scope); + sentry__scope_clear_attachments(scope); clear_scope_data(scope->data); + end_scope_notify(scope); unlock_scope_notify(scope); sentry__scope_flush(scope); } @@ -1011,6 +1090,7 @@ sentry_scope_clone(const sentry_scope_t *scope) sentry_free(clone); return NULL; } + clone->parent = sentry__scope_incref(scope->parent); return clone; } @@ -1022,6 +1102,18 @@ sentry__scope_load_propagation_context(const sentry_scope_t *scope) propagation_context = sentry__value_clone(scope->data->propagation_context); } + if (scope->parent) { + sentry_value_t parent + = sentry__scope_load_propagation_context(scope->parent); + // TODO: optimize sentry__value_merge_objects_shallow for empty + // destinations + if (!sentry_value_get_length(propagation_context)) { + sentry_value_decref(propagation_context); + return parent; + } + sentry__value_merge_objects_shallow(propagation_context, parent); + sentry_value_decref(parent); + } return propagation_context; } @@ -1051,6 +1143,9 @@ sentry__scope_load_trace_context(const sentry_scope_t *scope) trace_context = sentry__value_clone( sentry_value_get_by_key(scope->data->propagation_context, "trace")); } + if (sentry_value_is_null(trace_context) && scope->parent) { + return sentry__scope_load_trace_context(scope->parent); + } return trace_context; } @@ -1069,8 +1164,14 @@ bool sentry__scope_is_trace_managed(const sentry_scope_t *scope) { bool managed = false; + bool has_trace = false; SENTRY_SCOPE_READ_LOCK (scope->data) { managed = scope->data->trace_managed; + has_trace = !sentry_value_is_null( + sentry_value_get_by_key(scope->data->propagation_context, "trace")); + } + if (!has_trace && scope->parent) { + return sentry__scope_is_trace_managed(scope->parent); } return managed; } @@ -1087,8 +1188,36 @@ sentry_value_t sentry__scope_load_dsc(const sentry_scope_t *scope) { sentry_value_t dsc = sentry_value_new_null(); - SENTRY_SCOPE_READ_LOCK (scope->data) { - dsc = sentry__value_clone(scope->data->dynamic_sampling_context); + bool frozen = false; + for (const sentry_scope_t *cur = scope; cur; cur = cur->parent) { + bool inherit = false; + SENTRY_SCOPE_READ_LOCK (cur->data) { + frozen + = sentry_value_is_frozen(cur->data->dynamic_sampling_context); + inherit = !frozen && cur->parent + && !sentry_value_get_length( + cur->data->dynamic_sampling_context); + if (!inherit) { + dsc = sentry__value_clone(cur->data->dynamic_sampling_context); + } + } + if (!inherit) { + break; + } + } + if (!frozen && sentry_value_get_type(dsc) == SENTRY_VALUE_TYPE_OBJECT) { + sentry_value_t release = sentry__scope_ref_release(scope); + if (sentry_value_is_null(release)) { + sentry_value_remove_by_key(dsc, "release"); + } else { + sentry_value_set_by_key(dsc, "release", release); + } + sentry_value_t environment = sentry__scope_ref_environment(scope); + if (sentry_value_is_null(environment)) { + sentry_value_remove_by_key(dsc, "environment"); + } else { + sentry_value_set_by_key(dsc, "environment", environment); + } } return dsc; } @@ -1097,10 +1226,13 @@ void sentry__scope_foreach_dsc(const sentry_scope_t *scope, sentry_value_foreach_key_value_function_t callback, void *userdata) { - SENTRY_SCOPE_READ_LOCK (scope->data) { - sentry_value_foreach_key_value( - scope->data->dynamic_sampling_context, callback, userdata); + if (sentry_scope_begin_read(scope)) { + return; } + sentry_value_t dsc = sentry__scope_load_dsc(scope); + sentry_value_foreach_key_value(dsc, callback, userdata); + sentry_value_decref(dsc); + sentry_scope_end_read(scope); } void @@ -1218,15 +1350,20 @@ sentry__symbolize_stacktrace(sentry_value_t stacktrace) #endif static sentry_value_t -load_span_or_transaction_trace_context(const sentry_scope_data_t *scope_data) +load_span_or_transaction_trace_context(const sentry_scope_t *scope) { // prep contexts sourced from scope; data about transaction on scope needs // to be extracted and inserted sentry_value_t value = sentry_value_new_null(); - if (scope_data->span) { - value = scope_data->span->inner; - } else if (scope_data->transaction_object) { - value = scope_data->transaction_object->inner; + sentry_span_t *span = sentry__scope_ref_span(scope); + sentry_transaction_t *transaction = NULL; + if (span) { + value = span->inner; + } else { + transaction = sentry__scope_ref_transaction_object(scope); + if (transaction) { + value = transaction->inner; + } } sentry_value_t trace = sentry__value_get_trace_context(value); @@ -1236,6 +1373,8 @@ load_span_or_transaction_trace_context(const sentry_scope_data_t *scope_data) sentry_value_set_by_key(trace, "data", sentry__value_clone(data)); } } + sentry__span_decref(span); + sentry__transaction_decref(transaction); return trace; } @@ -1255,6 +1394,10 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, const sentry_options_t *options, sentry_value_t event, sentry_scope_mode_t mode) { + if (sentry_scope_begin_read(scope)) { + return; + } + #define IS_NULL(Key) sentry_value_is_null(sentry_value_get_by_key(event, Key)) #define SET(Key, Value) sentry_value_set_by_key(event, Key, Value) #define PLACE_STRING(Key, Source) \ @@ -1283,63 +1426,49 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, } \ } while (0) - const sentry_scope_data_t *data = scope->data; bool is_transaction = sentry__event_is_transaction(event); sentry_value_t event_contexts = sentry_value_get_by_key(event, "contexts"); - sentry_value_t release = sentry_value_new_null(); - sentry_value_t environment = sentry_value_new_null(); - sentry_level_t level = SENTRY_LEVEL_ERROR; - sentry_value_t user = sentry_value_new_null(); - sentry_value_t fingerprint = sentry_value_new_null(); - sentry_value_t transaction = sentry_value_new_null(); - sentry_value_t client_sdk = sentry_value_new_null(); - sentry_value_t contexts = sentry_value_new_null(); + sentry_value_t release = sentry__scope_ref_release(scope); + sentry_value_t environment = sentry__scope_ref_environment(scope); + sentry_level_t level = sentry__scope_get_level(scope); + sentry_value_t user = sentry__scope_ref_user(scope); + sentry_value_t fingerprint = sentry__scope_ref_fingerprint(scope); + sentry_value_t transaction = sentry__scope_ref_transaction(scope); + sentry_value_t client_sdk = sentry__scope_ref_client_sdk(scope); + sentry_value_t tags = sentry__scope_load_tags(scope); + sentry_value_t extra = sentry__scope_load_extra(scope); + sentry_value_t contexts = sentry__scope_load_contexts(scope); sentry_value_t scope_trace = sentry_value_new_null(); sentry_value_t propagation_context = sentry_value_new_null(); sentry_value_t scope_breadcrumbs = sentry_value_new_null(); - SENTRY_SCOPE_READ_LOCK (data) { - release = sentry_value_incref(data->release); - environment = sentry_value_incref(data->environment); - level = data->level; - user = sentry_value_incref(data->user); - fingerprint = sentry_value_incref(data->fingerprint); - transaction = sentry_value_incref(data->transaction); - client_sdk = sentry_value_incref(data->client_sdk); - - sentry_value_t event_tags = sentry_value_get_by_key(event, "tags"); - if (sentry_value_is_null(event_tags)) { - if (!sentry_value_is_null(scope->data->tags)) { - sentry_value_set_by_key( - event, "tags", sentry__value_clone(scope->data->tags)); - } - } else { - sentry__value_merge_objects(event_tags, scope->data->tags); + if (!is_transaction) { + scope_trace = load_span_or_transaction_trace_context(scope); + if (sentry_value_is_null(scope_trace) + && sentry_value_is_null( + sentry_value_get_by_key(event_contexts, "trace"))) { + propagation_context = sentry__scope_load_propagation_context(scope); } + } + if (mode & SENTRY_SCOPE_BREADCRUMBS) { + scope_breadcrumbs = sentry__scope_breadcrumbs_to_list(scope); + } + sentry_scope_end_read(scope); - sentry_value_t event_extra = sentry_value_get_by_key(event, "extra"); - if (sentry_value_is_null(event_extra)) { - if (!sentry_value_is_null(scope->data->extra)) { - sentry_value_set_by_key( - event, "extra", sentry__value_clone(scope->data->extra)); - } - } else { - sentry__value_merge_objects(event_extra, scope->data->extra); - } + sentry_value_t event_tags = sentry_value_get_by_key(event, "tags"); + if (sentry_value_is_null(event_tags)) { + sentry_value_set_by_key(event, "tags", tags); + } else { + sentry__value_merge_objects(event_tags, tags); + sentry_value_decref(tags); + } - contexts = sentry__value_clone(data->contexts); - if (!is_transaction) { - scope_trace = load_span_or_transaction_trace_context(data); - if (sentry_value_is_null(scope_trace) - && sentry_value_is_null( - sentry_value_get_by_key(event_contexts, "trace"))) { - propagation_context - = sentry__value_clone(data->propagation_context); - } - } - if (mode & SENTRY_SCOPE_BREADCRUMBS) { - scope_breadcrumbs = sentry__ringbuffer_to_list(data->breadcrumbs); - } + sentry_value_t event_extra = sentry_value_get_by_key(event, "extra"); + if (sentry_value_is_null(event_extra)) { + sentry_value_set_by_key(event, "extra", extra); + } else { + sentry__value_merge_objects(event_extra, extra); + sentry_value_decref(extra); } PLACE_STRING("platform", "native"); @@ -1442,11 +1571,33 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, #undef PLACE_STRING #undef SET #undef IS_NULL + + if (options->run && options->run->installation_id) { + sentry_value_t usr = sentry_value_get_by_key(event, "user"); + if (sentry_value_is_null(usr)) { + usr = sentry_value_new_object(); + sentry_value_set_by_key(event, "user", usr); + } + if (sentry_value_get_type(usr) == SENTRY_VALUE_TYPE_OBJECT + && sentry_value_is_null(sentry_value_get_by_key(usr, "id"))) { + sentry_value_set_by_key(usr, "id", + sentry_value_new_string(options->run->installation_id)); + } + } } void sentry_scope_add_breadcrumb(sentry_scope_t *scope, sentry_value_t breadcrumb) { + sentry__mutex_lock(&scope->observers_lock); + const sentry_options_t *options = sentry__options_incref(scope->state); + sentry__mutex_unlock(&scope->observers_lock); + breadcrumb = sentry__invoke_before_breadcrumb(options, breadcrumb); + sentry_options_free((sentry_options_t *)options); + if (sentry_value_is_null(breadcrumb)) { + return; + } + bool added = false; lock_scope_notify(scope); SENTRY_SCOPE_WRITE_LOCK (scope->data) { @@ -1469,6 +1620,15 @@ sentry__scope_breadcrumbs_to_list(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { breadcrumbs = sentry__ringbuffer_to_list(scope->data->breadcrumbs); } + if (scope->parent) { + sentry_value_t parent + = sentry__scope_breadcrumbs_to_list(scope->parent); + if (sentry_value_get_length(parent)) { + sentry__value_replace(&breadcrumbs, + sentry__value_merge_breadcrumbs(breadcrumbs, parent, SIZE_MAX)); + } + sentry_value_decref(parent); + } return breadcrumbs; } @@ -1479,6 +1639,9 @@ sentry__scope_ref_user(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { user = sentry_value_incref(scope->data->user); } + if (sentry_value_is_null(user) && scope->parent) { + return sentry__scope_ref_user(scope->parent); + } return user; } @@ -1501,6 +1664,16 @@ sentry__scope_load_tags(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { tags = sentry__value_clone(scope->data->tags); } + if (scope->parent) { + sentry_value_t parent = sentry__scope_load_tags(scope->parent); + // TODO: optimize sentry__value_merge_objects for empty destinations + if (!sentry_value_get_length(tags)) { + sentry_value_decref(tags); + return parent; + } + sentry__value_merge_objects(tags, parent); + sentry_value_decref(parent); + } return tags; } @@ -1620,6 +1793,16 @@ sentry__scope_load_extra(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { extra = sentry__value_clone(scope->data->extra); } + if (scope->parent) { + sentry_value_t parent = sentry__scope_load_extra(scope->parent); + // TODO: optimize sentry__value_merge_objects for empty destinations + if (!sentry_value_get_length(extra)) { + sentry_value_decref(extra); + return parent; + } + sentry__value_merge_objects(extra, parent); + sentry_value_decref(parent); + } return extra; } @@ -1734,6 +1917,17 @@ sentry__scope_load_attributes(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { attributes = sentry__value_clone(scope->data->attributes); } + if (scope->parent) { + sentry_value_t parent = sentry__scope_load_attributes(scope->parent); + // TODO: optimize sentry__value_merge_objects_shallow for empty + // destinations + if (!sentry_value_get_length(attributes)) { + sentry_value_decref(attributes); + return parent; + } + sentry__value_merge_objects_shallow(attributes, parent); + sentry_value_decref(parent); + } return attributes; } @@ -1763,6 +1957,16 @@ sentry__scope_load_contexts(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { contexts = sentry__value_clone(scope->data->contexts); } + if (scope->parent) { + sentry_value_t parent = sentry__scope_load_contexts(scope->parent); + // TODO: optimize sentry__value_merge_objects for empty destinations + if (!sentry_value_get_length(contexts)) { + sentry_value_decref(contexts); + return parent; + } + sentry__value_merge_objects(contexts, parent); + sentry_value_decref(parent); + } return contexts; } @@ -1895,8 +2099,6 @@ sentry_scope_set_release_n( SENTRY_SCOPE_WRITE_LOCK (scope->data) { sentry__value_replace( &scope->data->release, sentry_value_incref(value)); - sentry_value_set_by_key(scope->data->dynamic_sampling_context, - "release", sentry_value_incref(value)); } SENTRY_SCOPE_NOTIFY_OWNED(scope, set_release, value); unlock_scope_notify(scope); @@ -1919,8 +2121,6 @@ sentry_scope_set_environment_n( SENTRY_SCOPE_WRITE_LOCK (scope->data) { sentry__value_replace( &scope->data->environment, sentry_value_incref(value)); - sentry_value_set_by_key(scope->data->dynamic_sampling_context, - "environment", sentry_value_incref(value)); } SENTRY_SCOPE_NOTIFY_OWNED(scope, set_environment, value); unlock_scope_notify(scope); @@ -1968,6 +2168,9 @@ sentry__scope_ref_fingerprint(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { fingerprint = sentry_value_incref(scope->data->fingerprint); } + if (sentry_value_is_null(fingerprint) && scope->parent) { + return sentry__scope_ref_fingerprint(scope->parent); + } return fingerprint; } @@ -2061,10 +2264,14 @@ sentry_scope_remove_fingerprint(sentry_scope_t *scope) sentry_level_t sentry__scope_get_level(const sentry_scope_t *scope) { - sentry_level_t level = SENTRY_LEVEL_ERROR; + sentry_level_t level = SENTRY_LEVEL_NONE; SENTRY_SCOPE_READ_LOCK (scope->data) { level = scope->data->level; } + if (level == SENTRY_LEVEL_NONE) { + return scope->parent ? sentry__scope_get_level(scope->parent) + : SENTRY_LEVEL_ERROR; + } return level; } @@ -2075,6 +2282,9 @@ sentry__scope_ref_client_sdk(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { client_sdk = sentry_value_incref(scope->data->client_sdk); } + if (sentry_value_is_null(client_sdk) && scope->parent) { + return sentry__scope_ref_client_sdk(scope->parent); + } return client_sdk; } @@ -2107,23 +2317,43 @@ sentry_value_t sentry__scope_load_attachments(const sentry_scope_t *scope) { sentry_value_t attachments = sentry_value_new_null(); + if (!scope) { + return attachments; + } SENTRY_SCOPE_READ_LOCK (scope->data) { attachments = sentry__attachments_clone(scope->data->attachments); } + if (scope->parent) { + sentry_value_t parent = sentry__scope_load_attachments(scope->parent); + sentry__attachments_extend(&attachments, parent); + sentry_value_decref(parent); + } return attachments; } sentry_value_t sentry__scope_add_attachment(sentry_scope_t *scope, sentry_value_t attachment) { + if (!sentry__attachment_validate(attachment)) { + sentry_value_decref(attachment); + return sentry_value_new_null(); + } bool did_add = false; - sentry_value_t added = sentry_value_new_null(); lock_scope_notify(scope); - SENTRY_SCOPE_WRITE_LOCK (scope->data) { - size_t len = sentry_value_get_length(scope->data->attachments); - added = sentry__attachments_add(&scope->data->attachments, attachment); - did_add = !sentry_value_is_null(added) - && sentry_value_get_length(scope->data->attachments) > len; + sentry_value_t attachments = sentry__scope_load_attachments(scope); + sentry_value_t added = sentry__attachments_find(attachments, attachment); + sentry_value_decref(attachments); + if (sentry_value_is_null(added)) { + SENTRY_SCOPE_WRITE_LOCK (scope->data) { + size_t len = sentry_value_get_length(scope->data->attachments); + added = sentry__attachments_add( + &scope->data->attachments, attachment); + did_add = !sentry_value_is_null(added) + && sentry_value_get_length(scope->data->attachments) > len; + } + } else { + sentry_value_freeze(attachment); + sentry_value_decref(attachment); } if (did_add) { SENTRY_SCOPE_NOTIFY(scope, add_attachment, added); @@ -2192,9 +2422,14 @@ sentry_transaction_t * sentry__scope_ref_transaction_object(const sentry_scope_t *scope) { sentry_transaction_t *transaction = NULL; + bool has_binding = false; SENTRY_SCOPE_READ_LOCK (scope->data) { transaction = scope->data->transaction_object; sentry__transaction_incref(transaction); + has_binding = scope->data->span || scope->data->transaction_object; + } + if (!has_binding && scope->parent) { + return sentry__scope_ref_transaction_object(scope->parent); } return transaction; } @@ -2274,9 +2509,14 @@ sentry_span_t * sentry__scope_ref_span(const sentry_scope_t *scope) { sentry_span_t *span = NULL; + bool has_binding = false; SENTRY_SCOPE_READ_LOCK (scope->data) { span = scope->data->span; sentry__span_incref(span); + has_binding = scope->data->span || scope->data->transaction_object; + } + if (!has_binding && scope->parent) { + return sentry__scope_ref_span(scope->parent); } return span; } @@ -2285,13 +2525,18 @@ sentry_value_t sentry__scope_load_span_or_transaction(const sentry_scope_t *scope) { sentry_value_t value = sentry_value_new_null(); + bool has_binding = false; SENTRY_SCOPE_READ_LOCK (scope->data) { + has_binding = scope->data->span || scope->data->transaction_object; if (scope->data->span) { value = sentry__value_clone(scope->data->span->inner); } else if (scope->data->transaction_object) { value = sentry__value_clone(scope->data->transaction_object->inner); } } + if (!has_binding && scope->parent) { + return sentry__scope_load_span_or_transaction(scope->parent); + } return value; } @@ -2413,6 +2658,9 @@ sentry__scope_ref_release(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { release = sentry_value_incref(scope->data->release); } + if (sentry_value_is_null(release) && scope->parent) { + return sentry__scope_ref_release(scope->parent); + } return release; } @@ -2423,6 +2671,9 @@ sentry__scope_ref_environment(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { environment = sentry_value_incref(scope->data->environment); } + if (sentry_value_is_null(environment) && scope->parent) { + return sentry__scope_ref_environment(scope->parent); + } return environment; } @@ -2433,6 +2684,9 @@ sentry__scope_ref_transaction(const sentry_scope_t *scope) SENTRY_SCOPE_READ_LOCK (scope->data) { transaction = sentry_value_incref(scope->data->transaction); } + if (sentry_value_is_null(transaction) && scope->parent) { + return sentry__scope_ref_transaction(scope->parent); + } return transaction; } @@ -2501,72 +2755,77 @@ sentry_scope_attach_bytesw_n(sentry_scope_t *scope, const char *buf, } #endif +static sentry_value_t +load_context(const sentry_scope_t *scope, const char *key) +{ + sentry_value_t context = sentry_value_new_null(); + SENTRY_SCOPE_READ_LOCK (scope->data) { + context = sentry__value_clone( + sentry_value_get_by_key(scope->data->contexts, key)); + } + if (scope->parent) { + sentry_value_t parent = load_context(scope->parent, key); + if (sentry_value_is_null(context)) { + return parent; + } + sentry__value_merge_objects(context, parent); + sentry_value_decref(parent); + } + return context; +} + void sentry__scope_apply_to_telemetry(const sentry_scope_t *scope, sentry_value_t telemetry, sentry_value_t attributes) { - const sentry_scope_data_t *data = scope->data; - sentry_value_t os_name = sentry_value_new_null(); - sentry_value_t os_version = sentry_value_new_null(); - sentry_value_t user = sentry_value_new_null(); - sentry_value_t environment = sentry_value_new_null(); - sentry_value_t release = sentry_value_new_null(); + if (sentry_scope_begin_read(scope)) { + return; + } - SENTRY_SCOPE_READ_LOCK (data) { - sentry__value_merge_objects_shallow(attributes, data->attributes); - - sentry_value_t trace_id = sentry_value_get_by_key( - sentry_value_get_by_key(data->propagation_context, "trace"), - "trace_id"); - - sentry_value_t parent_span_id = sentry_value_new_object(); - bool has_parent_span = false; - if (data->transaction_object) { - sentry_value_t span_id = sentry_value_get_by_key( - data->transaction_object->inner, "span_id"); - sentry_value_set_by_key( - parent_span_id, "value", sentry_value_incref(span_id)); - trace_id = sentry_value_get_by_key( - data->transaction_object->inner, "trace_id"); - has_parent_span = true; - } else if (data->span) { - sentry_value_t span_id - = sentry_value_get_by_key(data->span->inner, "span_id"); - sentry_value_set_by_key( - parent_span_id, "value", sentry_value_incref(span_id)); - trace_id = sentry_value_get_by_key(data->span->inner, "trace_id"); - has_parent_span = true; - } + for (const sentry_scope_t *cur = scope; cur; cur = cur->parent) { + sentry__value_merge_objects_shallow(attributes, cur->data->attributes); + } + sentry_span_t *span = sentry__scope_ref_span(scope); + sentry_transaction_t *transaction + = sentry__scope_ref_transaction_object(scope); + sentry_value_t trace; + if (span) { + trace = span->inner; + } else if (transaction) { + trace = transaction->inner; + } else { + trace = sentry__scope_load_trace_context(scope); + } + sentry_value_t parent_span_id = sentry_value_new_null(); + sentry_value_t trace_id = sentry_value_get_by_key_owned(trace, "trace_id"); + if (span || transaction) { + parent_span_id = sentry_value_get_by_key_owned(trace, "span_id"); + } else { + sentry_value_decref(trace); + } + sentry__span_decref(span); + sentry__transaction_decref(transaction); + sentry_value_t os_context = load_context(scope, "os"); + sentry_value_t os_name = sentry_value_get_by_key_owned(os_context, "name"); + sentry_value_t os_version + = sentry_value_get_by_key_owned(os_context, "version"); + sentry_value_t user = sentry__scope_ref_user(scope); + sentry_value_t environment = sentry__scope_ref_environment(scope); + sentry_value_t release = sentry__scope_ref_release(scope); + sentry_scope_end_read(scope); + sentry_value_decref(os_context); + + if (!sentry_value_is_null(parent_span_id)) { + sentry__value_add_attribute(attributes, parent_span_id, "string", + "sentry.trace.parent_span_id"); + } + if (!sentry_value_is_null(trace_id) + && sentry_value_is_null( + sentry_value_get_by_key(telemetry, "trace_id"))) { sentry_value_set_by_key( - parent_span_id, "type", sentry_value_new_string("string")); - if (has_parent_span - && sentry_value_is_null(sentry_value_get_by_key( - attributes, "sentry.trace.parent_span_id"))) { - sentry_value_set_by_key( - attributes, "sentry.trace.parent_span_id", parent_span_id); - } else { - sentry_value_decref(parent_span_id); - } - if (!sentry_value_is_null(trace_id) - && sentry_value_is_null( - sentry_value_get_by_key(telemetry, "trace_id"))) { - sentry_value_set_by_key( - telemetry, "trace_id", sentry_value_incref(trace_id)); - } - - sentry_value_t os_context - = sentry_value_get_by_key(data->contexts, "os"); - if (!sentry_value_is_null(os_context)) { - os_name = sentry_value_incref( - sentry_value_get_by_key(os_context, "name")); - os_version = sentry_value_incref( - sentry_value_get_by_key(os_context, "version")); - } - - user = sentry_value_incref(data->user); - environment = sentry_value_incref(data->environment); - release = sentry_value_incref(data->release); + telemetry, "trace_id", sentry_value_incref(trace_id)); } + sentry_value_decref(trace_id); if (!sentry_value_is_null(os_name)) { sentry__value_add_attribute(attributes, os_name, "string", "os.name"); diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 090c64e45c..2baabc8f50 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -56,6 +56,7 @@ typedef struct sentry_scope_data_s sentry_scope_data_t; * This represents the current scope. */ struct sentry_scope_s { + // negative for static scopes long refcount; sentry_scope_data_t *data; @@ -66,6 +67,8 @@ struct sentry_scope_s { bool pending_flush; void (*flush_func)(sentry_scope_t *scope); void *state; + // immutable after construction + const sentry_scope_t *parent; // Whether this scope is single-use. A capture function frees a one-shot // scope after applying it. @@ -89,15 +92,14 @@ typedef enum { } sentry_scope_mode_t; /** - * This will return a new reference to the global scope, initializing it if - * needed. + * Replaces the isolation scope for SDK initialization. */ -sentry_scope_t *sentry__scope_getref(void); +bool sentry__scope_init(const sentry_options_t *options); /** * Increment the refcount and return the scope pointer. */ -sentry_scope_t *sentry__scope_incref(sentry_scope_t *scope); +sentry_scope_t *sentry__scope_incref(const sentry_scope_t *scope); /** * Decrement the refcount and free the scope when the last reference is @@ -106,7 +108,7 @@ sentry_scope_t *sentry__scope_incref(sentry_scope_t *scope); void sentry__scope_decref(sentry_scope_t *scope); /** - * This will free all the data attached to the global scope + * Releases the isolation scope after SDK shutdown. */ void sentry__scope_cleanup(void); @@ -121,21 +123,9 @@ void sentry__scope_set_one_shot(sentry_scope_t *scope, bool one_shot); */ void sentry__scope_free_one_shot(sentry_scope_t *scope); -/** - * Finish a read-only global scope access. - * This consumes the caller's scope reference. - */ -void sentry__scope_finish(sentry_scope_t *scope); - -/** - * Finish a mutable global scope access, flushing pending changes. - * This consumes the caller's scope reference. - */ -void sentry__scope_finish_mut(sentry_scope_t *scope); - /** * Flush scope changes. The flush is deferred until mutable access finishes. - * No-op for non-global scopes without a flush callback. + * No-op for scopes without a flush callback. */ void sentry__scope_flush(sentry_scope_t *scope); void sentry__scope_set_flush_func( @@ -143,8 +133,8 @@ void sentry__scope_set_flush_func( void sentry__scope_set_state(sentry_scope_t *scope, void *state); /** - * This will merge the requested data which is in the given `scope` to the given - * `event`. + * This will merge the requested data from `scope` and its parents to the given + * `event`, most specific first. * See `sentry_scope_mode_t` for the different types of data that can be * attached. */ @@ -169,7 +159,7 @@ sentry_level_t sentry__scope_get_level(const sentry_scope_t *scope); sentry_value_t sentry__scope_ref_client_sdk(const sentry_scope_t *scope); /** - * Returns an owned reference to the scope attachment list. + * Returns an owned list of attachments from the scope and its parents. * The caller must release it with `sentry_value_decref`. */ sentry_value_t sentry__scope_load_attachments(const sentry_scope_t *scope); @@ -228,14 +218,15 @@ bool sentry__scope_is_trace_managed(const sentry_scope_t *scope); void sentry__scope_set_trace_managed(sentry_scope_t *scope, bool managed); /** - * These are convenience macros to access the global scope inside a code block. + * These are convenience macros to access the isolation scope inside a code + * block. */ #define SENTRY_WITH_SCOPE(Scope) \ - for (const sentry_scope_t *Scope = sentry__scope_getref(); Scope; \ - sentry__scope_finish((sentry_scope_t *)Scope), Scope = NULL) + for (const sentry_scope_t *Scope = sentry_scope_acquire(); Scope; \ + sentry__scope_decref((sentry_scope_t *)Scope), Scope = NULL) #define SENTRY_WITH_SCOPE_MUT(Scope) \ - for (sentry_scope_t *Scope = sentry__scope_getref(); Scope; \ - sentry__scope_finish_mut(Scope), Scope = NULL) + for (sentry_scope_t *Scope = sentry_scope_acquire(); Scope; \ + sentry_scope_release(Scope), Scope = NULL) // TODO: adapt console SDKs to SENTRY_WITH_SCOPE_MUT and remove the alias #define SENTRY_WITH_SCOPE_MUT_NO_FLUSH(Scope) SENTRY_WITH_SCOPE_MUT (Scope) @@ -287,9 +278,8 @@ void sentry__scope_update_dsc( void sentry__scope_freeze_dsc(sentry_scope_t *scope, sentry_value_t incoming); /** - * Merges the given scope data into a telemetry item (a log or metric): its - * attributes, trace, user, etc. Existing values are kept, so this can be called - * for each scope in a precedence chain, most specific first (first write wins). + * Merges the scope and its parent scopes into a log or metric, most specific + * first. Existing values are kept. */ void sentry__scope_apply_to_telemetry(const sentry_scope_t *scope, sentry_value_t telemetry, sentry_value_t attributes); diff --git a/src/sentry_session.c b/src/sentry_session.c index bd69d6b0b1..ad4d68923e 100644 --- a/src/sentry_session.c +++ b/src/sentry_session.c @@ -226,7 +226,8 @@ sentry_start_session(void) sentry_end_session(); sentry_options_t *options = sentry__options_lock(); SENTRY_WITH_SCOPE (scope) { - if (options) { + if (options && !sentry_scope_begin_read(scope)) { + sentry__mutex_lock(&options->session_lock); options->session = sentry__session_new(scope); if (options->session) { sentry_value_t user = sentry__scope_ref_user(scope); @@ -236,6 +237,8 @@ sentry_start_session(void) sentry__run_write_session(options->run, options->session); sentry__session_notify_changed(options); } + sentry__mutex_unlock(&options->session_lock); + sentry_scope_end_read(scope); } } sentry__options_unlock(); @@ -245,8 +248,12 @@ void sentry__record_errors_on_current_session(uint32_t error_count) { sentry_options_t *options = sentry__options_lock(); - if (options && options->session) { - options->session->errors += error_count; + if (options) { + sentry__mutex_lock(&options->session_lock); + if (options->session) { + options->session->errors += error_count; + } + sentry__mutex_unlock(&options->session_lock); } sentry__options_unlock(); } @@ -257,12 +264,14 @@ sentry__end_session_internal(void) sentry_session_t *session = NULL; sentry_options_t *options = sentry__options_lock(); if (options) { + sentry__mutex_lock(&options->session_lock); session = options->session; options->session = NULL; sentry__run_clear_session(options->run); if (session) { sentry__session_notify_changed(options); } + sentry__mutex_unlock(&options->session_lock); } sentry__options_unlock(); diff --git a/src/sentry_session.h b/src/sentry_session.h index acc96deb54..6e32394ee4 100644 --- a/src/sentry_session.h +++ b/src/sentry_session.h @@ -71,7 +71,7 @@ void sentry__session_sync_user(sentry_session_t *session, sentry_value_t user, /** * This notifies the backend that the current session of `options` started, - * changed or ended. Must be called with the options lock held. + * changed or ended. Must be called with the session lock held. */ void sentry__session_notify_changed(const sentry_options_t *options); diff --git a/tests/assertions.py b/tests/assertions.py index 9176a63e2f..9873cf3aac 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -610,6 +610,7 @@ def assert_crashpad_upload( expect_breadcrumbs=True, expect_default_scope=True, breadcrumb_count=0, + expect_byte_attachment=None, ): multipart = gzip.decompress(req.get_data()) msg = email.message_from_bytes(bytes(str(req.headers), encoding="utf8") + multipart) @@ -624,9 +625,13 @@ def assert_crashpad_upload( assert_event_meta(attachments.event, integrations=["crashpad"]) if expect_attachment: assert attachments.cmake_cache > 0 - assert attachments.bytes_bin == b"\xc0\xff\xee" else: assert attachments.cmake_cache == -1 + if expect_byte_attachment is None: + expect_byte_attachment = expect_attachment + if expect_byte_attachment: + assert attachments.bytes_bin == b"\xc0\xff\xee" + else: assert attachments.bytes_bin == None if expect_view_hierarchy: assert_attachment_content_view_hierarchy(attachments.view_hierarchy) diff --git a/tests/fixtures/integration.c b/tests/fixtures/integration.c index 1a6fca555d..5bcfa86d33 100644 --- a/tests/fixtures/integration.c +++ b/tests/fixtures/integration.c @@ -1239,6 +1239,25 @@ main(int argc, char **argv) sentry_set_environment("updated-environment"); } + if (has_arg(argc, argv, "acquire-scope")) { + sentry_scope_t *scope = sentry_scope_acquire(); + sentry_scope_set_tag(scope, "acquired", "yes"); + sentry_scope_set_release(scope, "scope-release"); + sentry_scope_set_environment(scope, "scope-environment"); + sentry_scope_set_user( + scope, sentry_value_new_user("scope-user", NULL, NULL, NULL)); + sentry_scope_add_breadcrumb( + scope, sentry_value_new_breadcrumb(NULL, "acquired breadcrumb")); + sentry_scope_attach_bytes(scope, "scope", 5, "scope.txt"); + sentry_scope_release(scope); + } + + if (has_arg(argc, argv, "clear-scope")) { + sentry_scope_t *scope = sentry_scope_acquire(); + sentry_scope_clear(scope); + sentry_scope_release(scope); + } + if (has_arg(argc, argv, "start-session")) { sentry_start_session(); } diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index 3ceff5e880..a68f149880 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -617,14 +617,14 @@ def test_crashpad_dumping_crash(cmake, httpserver, run_args, build_args): envelope = Envelope.deserialize(session) assert_session(envelope, {"status": "crashed", "errors": 1}) - expect_attachments = not any( - arg in run_args for arg in ("before-send", "on-crash", "clear-attachments") - ) + expect_attachments = not any(arg in run_args for arg in ("before-send", "on-crash")) + cleared = "clear-attachments" in run_args attachments = assert_crashpad_upload( multipart, expect_attachment=expect_attachments, - expect_view_hierarchy=expect_attachments, + expect_view_hierarchy=expect_attachments and not cleared, breadcrumb_count=101, + expect_byte_attachment=expect_attachments and not cleared, ) callback = next( (arg for arg in run_args if arg in ("before-send", "on-crash")), None @@ -1491,3 +1491,46 @@ def test_crashpad_breadcrumbs(cmake, httpserver, count, reinstall, crash_arg): if crash_arg == "fastfail": assert wait_for_no_werfault() + + +@pytest.mark.parametrize("clear", [False, True]) +def test_crashpad_acquired_scope(cmake, httpserver, clear): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "crashpad"}) + httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK") + args = [ + "log", + "no-setup", + "attachment", + "acquire-scope", + "crashpad-wait-for-upload", + "crash", + ] + if clear: + args.append("clear-scope") + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_test_integration", + args, + expect_failure=True, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + assert waiting.result + + attachments = assert_crashpad_upload( + httpserver.log[0][0], + expect_attachment=True, + expect_breadcrumbs=False, + expect_default_scope=False, + expect_byte_attachment=not clear, + ) + event = attachments.event + assert event["release"] == ("test-example-release" if clear else "scope-release") + assert event["environment"] == ("development" if clear else "scope-environment") + assert ("acquired" in event.get("tags", {})) == (not clear) + assert any( + crumb.get("message") == "acquired breadcrumb" + for crumb in attachments.breadcrumb1 + attachments.breadcrumb2 + ) == (not clear) + if not clear: + assert event["user"]["id"] == "scope-user" diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 664443a817..fdbc22ab0e 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -1692,3 +1692,37 @@ def test_early_init(cmake, backend): result = run(tmp_path, "sentry_early_init", []) assert result.returncode == 0 + + +@pytest.mark.parametrize("capture", ["capture-event", "capture-with-scope"]) +@pytest.mark.parametrize("clear", [False, True]) +def test_capture_acquired_scope(cmake, httpserver, capture, clear): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + args = ["log", "no-setup", "attachment", "acquire-scope", capture] + if clear: + args.append("clear-scope") + run( + tmp_path, + "sentry_test_integration", + args, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + assert len(httpserver.log) == 1 + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + event = envelope.get_event() + assert event["release"] == ("test-example-release" if clear else "scope-release") + assert event["environment"] == ("development" if clear else "scope-environment") + assert event["sdk"]["version"] == SENTRY_VERSION + assert event["contexts"]["os"]["name"] + assert ("acquired" in event.get("tags", {})) == (not clear) + assert any( + crumb.get("message") == "acquired breadcrumb" + for crumb in (event.get("breadcrumbs") or []) + ) == (not clear) + filenames = {item.headers.get("filename") for item in envelope} + assert "CMakeCache.txt" in filenames + assert ("scope.txt" in filenames) == (not clear) + if not clear: + assert event["user"]["id"] == "scope-user" diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 9cc8ce6bd6..5eb6d9d39c 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -1732,3 +1732,36 @@ def daemon_log(): child.kill() child.wait() assert wait_for(lambda: not list(db_dir.glob("*.run.daemon.lock"))) + + +@pytest.mark.parametrize("clear", [False, True]) +def test_native_acquired_scope(cmake, httpserver, clear): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "native"}) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + args = ["log", "no-setup", "attachment", "acquire-scope", "crash"] + if clear: + args.append("clear-scope") + with httpserver.wait(timeout=10) as waiting: + run_crash( + tmp_path, + "sentry_test_integration", + args, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + assert waiting.result + + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + assert_native_crash(envelope) + event = envelope.get_event() + assert event["release"] == ("test-example-release" if clear else "scope-release") + assert event["environment"] == ("development" if clear else "scope-environment") + assert event["sdk"]["name"] == "sentry.native" + assert event["contexts"]["os"]["name"] + assert ("acquired" in event.get("tags", {})) == (not clear) + assert any( + crumb.get("message") == "acquired breadcrumb" + for crumb in (event.get("breadcrumbs") or []) + ) == (not clear) + filenames = {item.headers.get("filename") for item in envelope} + assert "CMakeCache.txt" in filenames + assert ("scope.txt" in filenames) == (not clear) diff --git a/tests/unit/test_attachments.c b/tests/unit/test_attachments.c index 1bd0d17d33..f75c14ecd7 100644 --- a/tests/unit/test_attachments.c +++ b/tests/unit/test_attachments.c @@ -169,6 +169,7 @@ SENTRY_TEST(attachments_add_dedupe) sentry_attach_file(SENTRY_TEST_PATH_PREFIX ".a.txt"); sentry_attach_file(SENTRY_TEST_PATH_PREFIX ".b.txt"); sentry_attach_file(SENTRY_TEST_PATH_PREFIX ".c.txt"); + sentry_attach_file(SENTRY_TEST_PATH_PREFIX ".c.txt"); #ifdef SENTRY_PLATFORM_WINDOWS sentry_attach_filew(SENTRY_TEST_PATH_PREFIX L".a.txt"); sentry_attach_filew(SENTRY_TEST_PATH_PREFIX L".b.txt"); @@ -194,10 +195,10 @@ SENTRY_TEST(attachments_add_dedupe) TEST_CHECK_STRING_EQUAL(serialized, "{}\n" + "{\"type\":\"attachment\",\"length\":3,\"filename\":\".c.txt\"}\nccc\n" "{\"type\":\"attachment\",\"length\":3,\"filename\":\".a.txt\"}\naaa\n" - "{\"type\":\"attachment\",\"length\":3,\"filename\":\".b.txt\"}\nbbb\n" - "{\"type\":\"attachment\",\"length\":3,\"filename\":\".c.txt\"}" - "\nccc"); + "{\"type\":\"attachment\",\"length\":3,\"filename\":\".b.txt\"}" + "\nbbb"); sentry_free(serialized); @@ -274,18 +275,19 @@ SENTRY_TEST(attachments_add_remove) TEST_CHECK_STRING_EQUAL(serialized, "{}\n" "{\"type\":\"attachment\",\"length\":3,\"filename\":\".a.txt\"}\naaa\n" + "{\"type\":\"attachment\",\"length\":3,\"filename\":\".c.txt\"}\nccc\n" "{\"type\":\"attachment\",\"length\":3,\"filename\":\".b.txt\"}" "\nbbb"); - sentry_free(serialized); - + sentry_attach_bytes("ddd", 3, ".d.txt"); sentry_clear_attachments(); envelope = sentry__envelope_new(); add_scope_attachments(envelope); - serialized = sentry_envelope_serialize(envelope, NULL); + char *cleared = sentry_envelope_serialize(envelope, NULL); sentry_envelope_free(envelope); - TEST_CHECK_STRING_EQUAL(serialized, "{}"); + TEST_CHECK_STRING_EQUAL(cleared, serialized); + sentry_free(cleared); sentry_free(serialized); sentry_close(); diff --git a/tests/unit/test_concurrency.c b/tests/unit/test_concurrency.c index e49bf89a35..d667bb4e22 100644 --- a/tests/unit/test_concurrency.c +++ b/tests/unit/test_concurrency.c @@ -280,7 +280,7 @@ SENTRY_TEST(concurrent_reinstall) // Holding the scope observer lock simulates an observer callback in // progress. Backend reinstall must wait until that callback finishes. - sentry_scope_t *scope = sentry__scope_getref(); + sentry_scope_t *scope = sentry_scope_acquire(); TEST_ASSERT(!!scope); sentry__mutex_lock(&scope->observers_lock); sentry_threadid_t thread; @@ -299,7 +299,7 @@ SENTRY_TEST(concurrent_reinstall) TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.shutdowns), 0); sentry__mutex_unlock(&scope->observers_lock); - sentry__scope_finish(scope); + sentry_scope_release(scope); sentry__thread_join(thread); sentry__thread_free(&thread); TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.shutdowns), 1); @@ -321,7 +321,7 @@ SENTRY_THREAD_FN scope_access_thread(void *data) { scope_cleanup_state_t *state = data; - sentry_scope_t *scope = sentry__scope_getref(); + sentry_scope_t *scope = sentry_scope_acquire(); sentry__mutex_lock(&state->lock); state->accesses++; @@ -337,7 +337,7 @@ scope_access_thread(void *data) SENTRY_WITH_SCOPE (nested) { (void)sentry__scope_get_level(nested); } - sentry__scope_finish(scope); + sentry_scope_release(scope); return 0; } @@ -392,14 +392,14 @@ SENTRY_TEST(scope_cleanup) sentry__cond_wait(&state.cleanup_signal, &state.lock); } sentry__cond_wait_timeout(&state.cleanup_signal, &state.lock, 250); - TEST_CHECK(!state.cleanup_finished); + TEST_CHECK(state.cleanup_finished); sentry_threadid_t late_thread; sentry__thread_init(&late_thread); TEST_ASSERT_INT_EQUAL( sentry__thread_spawn(&late_thread, scope_access_thread, &state), 0); sentry__cond_wait_timeout(&state.access_signal, &state.lock, 250); - TEST_CHECK_INT_EQUAL(state.accesses, 1); + TEST_CHECK_INT_EQUAL(state.accesses, 2); state.release_access = true; sentry__cond_wake_all(&state.access_signal); sentry__mutex_unlock(&state.lock); diff --git a/tests/unit/test_logs.c b/tests/unit/test_logs.c index 35960f1ace..39df5f8d01 100644 --- a/tests/unit/test_logs.c +++ b/tests/unit/test_logs.c @@ -607,6 +607,10 @@ SENTRY_TEST(scope_capture_log_attributes) sentry_set_attribute("from_global", string_attribute("global")); sentry_set_attribute("scope_over_global", string_attribute("global")); sentry_set_attribute("log_over_all", string_attribute("global")); + sentry_value_t os = sentry_value_new_object(); + sentry_value_set_by_key(os, "name", sentry_value_new_string("ParentOS")); + sentry_value_set_by_key(os, "version", sentry_value_new_string("1.0")); + sentry_set_context("os", os); sentry_scope_t *scope = sentry_scope_new(); sentry_scope_set_attribute(scope, "from_scope", string_attribute("scope")); @@ -614,6 +618,9 @@ SENTRY_TEST(scope_capture_log_attributes) scope, "scope_over_global", string_attribute("scope")); sentry_scope_set_attribute( scope, "log_over_all", string_attribute("scope")); + os = sentry_value_new_object(); + sentry_value_set_by_key(os, "name", sentry_value_new_string("ChildOS")); + sentry_scope_set_context(scope, "os", os); sentry_value_t attrs = sentry_value_new_object(); sentry_value_set_by_key(attrs, "from_log", string_attribute("log")); @@ -636,6 +643,8 @@ SENTRY_TEST(scope_capture_log_attributes) TEST_CHECK_STRING_EQUAL( attribute_value(log_attrs, "scope_over_global"), "scope"); TEST_CHECK_STRING_EQUAL(attribute_value(log_attrs, "log_over_all"), "log"); + TEST_CHECK_STRING_EQUAL(attribute_value(log_attrs, "os.name"), "ChildOS"); + TEST_CHECK_STRING_EQUAL(attribute_value(log_attrs, "os.version"), "1.0"); sentry_scope_free(scope); sentry_value_decref(captured_log); diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 579c13b28f..8d312f583d 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -67,6 +67,7 @@ typedef struct { sentry_value_t environment; sentry_value_t tag; int result; + sentry_options_t *options; } scope_access_thread_t; SENTRY_THREAD_FN @@ -74,12 +75,23 @@ read_scope_batch(void *data) { scope_access_thread_t *state = data; sentry__waitable_flag_set(&state->started); - state->result = sentry_scope_begin_read(state->scope); - if (state->result == 0) { - state->environment = sentry__scope_ref_environment(state->scope); - state->tag = scope_value_get_by_key( - sentry__scope_load_tags, state->scope, "version"); - sentry_scope_end_read(state->scope); + if (state->options) { + sentry_value_t event = sentry_value_new_event(); + sentry__scope_apply_to_event( + state->scope, state->options, event, SENTRY_SCOPE_NONE); + state->environment + = sentry_value_get_by_key_owned(event, "environment"); + state->tag = sentry_value_get_by_key_owned( + sentry_value_get_by_key(event, "tags"), "version"); + sentry_value_decref(event); + } else { + state->result = sentry_scope_begin_read(state->scope); + if (state->result == 0) { + state->environment = sentry__scope_ref_environment(state->scope); + state->tag = scope_value_get_by_key( + sentry__scope_load_tags, state->scope, "version"); + sentry_scope_end_read(state->scope); + } } sentry__waitable_flag_set(&state->finished); return 0; @@ -111,6 +123,30 @@ SENTRY_TEST(scope_write_exclusive) TEST_CHECK_STRING_EQUAL(sentry_value_as_string(state.tag), "new"); sentry_value_decref(state.environment); sentry_value_decref(state.tag); + + sentry_scope_clear(scope); + sentry_scope_set_tag(scope, "version", "old"); + sentry_scope_t *parent = sentry_scope_acquire(); + TEST_ASSERT(sentry_scope_begin_write(parent) == 0); + sentry_scope_set_environment(parent, "old"); + scope_access_thread_t apply + = { .scope = scope, .options = sentry_options_new() }; + sentry__waitable_flag_init(&apply.started); + sentry__waitable_flag_init(&apply.finished); + TEST_ASSERT(!sentry__thread_spawn(&thread, read_scope_batch, &apply)); + TEST_ASSERT(sentry__waitable_flag_wait(&apply.started, 1000)); + TEST_CHECK(!sentry__waitable_flag_wait(&apply.finished, 10)); + sentry_scope_set_tag(scope, "version", "new"); + sentry_scope_set_environment(parent, "new"); + sentry_scope_end_write(parent); + TEST_CHECK(sentry__waitable_flag_wait(&apply.finished, 1000)); + sentry__thread_join(thread); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(apply.environment), "new"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(apply.tag), "new"); + sentry_value_decref(apply.environment); + sentry_value_decref(apply.tag); + sentry_options_free(apply.options); + sentry_scope_release(parent); sentry_scope_free(scope); } @@ -183,6 +219,13 @@ SENTRY_TEST(scope_contexts) TEST_CHECK(sentry_value_is_null(removed)); sentry_value_decref(removed); + sentry_value_t contexts = sentry__scope_load_contexts(local_scope); + TEST_CHECK_JSON_VALUE( + sentry_value_get_by_key(contexts, "global"), "\"global\""); + TEST_CHECK_JSON_VALUE( + sentry_value_get_by_key(contexts, "scope"), "\"local\""); + sentry_value_decref(contexts); + // event: // {"all":"event","event":"event"} sentry_value_t event = sentry_value_new_object(); @@ -359,6 +402,26 @@ SENTRY_TEST(scope_propagation_context) // local scope applied first must not prevent the global scope // from adding the trace context afterwards sentry_scope_t *local_scope = sentry_local_scope_new(); + sentry_value_t trace = sentry__scope_load_trace_context(local_scope); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(trace, "trace_id")), + "aaaabbbbccccddddeeeeffff00001111"); + TEST_CHECK(!sentry__scope_is_trace_managed(local_scope)); + sentry__scope_update_dsc(local_scope, options); + sentry_value_t dsc = sentry__scope_load_dsc(local_scope); + TEST_CHECK( + sentry_value_as_double(sentry_value_get_by_key(dsc, "sample_rand")) + == sentry_value_as_double( + sentry_value_get_by_key(trace, "sample_rand"))); + sentry_value_decref(dsc); + sentry_value_decref(trace); + sentry_value_t propagation + = sentry__scope_load_propagation_context(local_scope); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(propagation, "trace"), "trace_id")), + "aaaabbbbccccddddeeeeffff00001111"); + sentry_value_decref(propagation); sentry_value_t event = sentry_value_new_object(); sentry__scope_apply_to_event( local_scope, options, event, SENTRY_SCOPE_NONE); @@ -366,6 +429,13 @@ SENTRY_TEST(scope_propagation_context) global_scope, options, event, SENTRY_SCOPE_NONE); TEST_CHECK_EVENT_TRACE_ID_EQUAL( event, "aaaabbbbccccddddeeeeffff00001111"); + + sentry__scope_regenerate_propagation_context(local_scope); + TEST_CHECK(sentry__scope_is_trace_managed(local_scope)); + propagation = sentry__scope_load_propagation_context(local_scope); + TEST_CHECK(sentry_value_is_null(sentry_value_get_by_key( + sentry_value_get_by_key(propagation, "trace"), "parent_span_id"))); + sentry_value_decref(propagation); sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -462,6 +532,13 @@ SENTRY_TEST(scope_extra) TEST_CHECK(sentry_value_is_null(removed)); sentry_value_decref(removed); + sentry_value_t extra = sentry__scope_load_extra(local_scope); + TEST_CHECK_JSON_VALUE( + sentry_value_get_by_key(extra, "global"), "\"global\""); + TEST_CHECK_JSON_VALUE( + sentry_value_get_by_key(extra, "scope"), "\"local\""); + sentry_value_decref(extra); + // event: // {"all":"event","event":"event"} sentry_value_t event = sentry_value_new_object(); @@ -551,6 +628,10 @@ SENTRY_TEST(scope_fingerprint) // local: // ["local1", "local2"] sentry_scope_t *local_scope = sentry_local_scope_new(); + sentry_value_t local_fingerprint + = sentry__scope_ref_fingerprint(local_scope); + TEST_CHECK_JSON_VALUE(local_fingerprint, "[\"global1\",\"global2\"]"); + sentry_value_decref(local_fingerprint); sentry_scope_set_fingerprint(local_scope, "local1", "local2", NULL); // event: @@ -577,8 +658,7 @@ SENTRY_TEST(scope_fingerprint) TEST_CHECK_JSON_VALUE(sentry_value_get_by_key(event, "fingerprint"), "[\"event1\",\"event2\"]"); - sentry_value_t local_fingerprint - = sentry__scope_ref_fingerprint(local_scope); + local_fingerprint = sentry__scope_ref_fingerprint(local_scope); sentry_scope_remove_fingerprint(local_scope); TEST_CHECK_INT_EQUAL(sentry_value_get_length(local_fingerprint), 2); TEST_CHECK_STRING_EQUAL( @@ -587,6 +667,10 @@ SENTRY_TEST(scope_fingerprint) "local1"); sentry_value_decref(local_fingerprint); + local_fingerprint = sentry__scope_ref_fingerprint(local_scope); + TEST_CHECK_JSON_VALUE(local_fingerprint, "[\"global1\",\"global2\"]"); + sentry_value_decref(local_fingerprint); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -710,9 +794,17 @@ SENTRY_TEST(scope_tags) // local: // {"all":"scope","scope":"scope","local":"local"} sentry_scope_t *local_scope = sentry_local_scope_new(); + sentry_value_t local_tags = sentry__scope_load_tags(local_scope); + sentry_value_set_by_key( + local_tags, "scope", sentry_value_new_string("detached")); + sentry_value_decref(local_tags); + local_tags = sentry__scope_load_tags(local_scope); + TEST_CHECK_JSON_VALUE( + sentry_value_get_by_key(local_tags, "scope"), "\"global\""); + sentry_value_decref(local_tags); sentry_scope_set_tag(local_scope, "all", "local"); sentry_scope_set_tag(local_scope, "scope", "overwritten"); - sentry_value_t local_tags = sentry_value_new_object(); + local_tags = sentry_value_new_object(); sentry_value_set_by_key( local_tags, "local", sentry_value_new_string("local")); sentry_value_set_by_key( @@ -731,6 +823,13 @@ SENTRY_TEST(scope_tags) TEST_CHECK(sentry_value_is_null(removed)); sentry_value_decref(removed); + local_tags = sentry__scope_load_tags(local_scope); + TEST_CHECK_JSON_VALUE( + sentry_value_get_by_key(local_tags, "global"), "\"global\""); + TEST_CHECK_JSON_VALUE( + sentry_value_get_by_key(local_tags, "scope"), "\"local\""); + sentry_value_decref(local_tags); + // event: // {"all":"event","event":"event"} sentry_value_t event = sentry_value_new_object(); @@ -797,6 +896,10 @@ SENTRY_TEST(scope_user) SENTRY_WITH_SCOPE (global_scope) { // local: {"id":"2","username":"local","email":"@local"} sentry_scope_t *local_scope = sentry_local_scope_new(); + sentry_value_t local_user = sentry__scope_ref_user(local_scope); + TEST_CHECK_JSON_VALUE(local_user, + "{\"id\":\"1\",\"username\":\"global\",\"email\":\"@global\"}"); + sentry_value_decref(local_user); sentry_scope_set_user( local_scope, sentry_value_new_user("2", "local", "@local", NULL)); @@ -817,13 +920,18 @@ SENTRY_TEST(scope_user) TEST_CHECK_JSON_VALUE(sentry_value_get_by_key(event, "user"), "{\"id\":\"3\",\"username\":\"event\"}"); - sentry_value_t local_user = sentry__scope_ref_user(local_scope); + local_user = sentry__scope_ref_user(local_scope); sentry_scope_set_user(local_scope, sentry_value_new_null()); TEST_CHECK_STRING_EQUAL( sentry_value_as_string(sentry_value_get_by_key(local_user, "id")), "2"); sentry_value_decref(local_user); + local_user = sentry__scope_ref_user(local_scope); + TEST_CHECK_JSON_VALUE(local_user, + "{\"id\":\"1\",\"username\":\"global\",\"email\":\"@global\"}"); + sentry_value_decref(local_user); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -958,7 +1066,11 @@ SENTRY_TEST(scope_level) SENTRY_WITH_SCOPE (global_scope) { // local: fatal sentry_scope_t *local_scope = sentry_local_scope_new(); + TEST_CHECK_INT_EQUAL( + sentry__scope_get_level(local_scope), SENTRY_LEVEL_WARNING); sentry_scope_set_level(local_scope, SENTRY_LEVEL_FATAL); + TEST_CHECK_INT_EQUAL( + sentry__scope_get_level(local_scope), SENTRY_LEVEL_FATAL); // event: debug sentry_value_t event = sentry_value_new_object(); @@ -975,6 +1087,25 @@ SENTRY_TEST(scope_level) local_scope, options, event, SENTRY_SCOPE_NONE); TEST_CHECK_LEVEL_EQUAL(event, "debug"); + sentry_scope_set_level(local_scope, SENTRY_LEVEL_ERROR); + TEST_CHECK_INT_EQUAL( + sentry__scope_get_level(local_scope), SENTRY_LEVEL_ERROR); + sentry_value_remove_by_key(event, "level"); + sentry__scope_apply_to_event( + local_scope, options, event, SENTRY_SCOPE_NONE); + TEST_CHECK_LEVEL_EQUAL(event, "error"); + + sentry_scope_clear(local_scope); + TEST_CHECK_INT_EQUAL( + sentry__scope_get_level(local_scope), SENTRY_LEVEL_WARNING); + sentry_value_remove_by_key(event, "level"); + sentry__scope_apply_to_event( + local_scope, options, event, SENTRY_SCOPE_NONE); + TEST_CHECK_LEVEL_EQUAL(event, "warning"); + sentry_set_level(SENTRY_LEVEL_DEBUG); + TEST_CHECK_INT_EQUAL( + sentry__scope_get_level(local_scope), SENTRY_LEVEL_DEBUG); + sentry_scope_free(local_scope); sentry_value_decref(event); } @@ -999,7 +1130,7 @@ SENTRY_TEST(scope_breadcrumbs) sentry_options_set_max_breadcrumbs(options, 5); sentry_init(options); - // global: ["global1", "global4"] + // isolation: ["global1", "global4"] sentry_add_breadcrumb(breadcrumb_ts("global1", 1)); sentry_add_breadcrumb(breadcrumb_ts("global4", 4)); @@ -1009,13 +1140,13 @@ SENTRY_TEST(scope_breadcrumbs) sentry_value_get_by_index(breadcrumbs, index), "message")), \ message) - SENTRY_WITH_SCOPE (global_scope) { + SENTRY_WITH_SCOPE (isolation_scope) { // event: null sentry_value_t event = sentry_value_new_object(); - // event <- global: ["global1", "global4"] + // event <- isolation: ["global1", "global4"] sentry__scope_apply_to_event( - global_scope, options, event, SENTRY_SCOPE_BREADCRUMBS); + isolation_scope, options, event, SENTRY_SCOPE_BREADCRUMBS); sentry_value_t result = sentry_value_get_by_key(event, "breadcrumbs"); TEST_CHECK(sentry_value_get_type(result) == SENTRY_VALUE_TYPE_LIST); @@ -1026,7 +1157,7 @@ SENTRY_TEST(scope_breadcrumbs) sentry_value_decref(event); } - SENTRY_WITH_SCOPE (global_scope) { + SENTRY_WITH_SCOPE (isolation_scope) { // event: ["event3", "event5"] sentry_value_t event = sentry_value_new_object(); { @@ -1036,9 +1167,9 @@ SENTRY_TEST(scope_breadcrumbs) sentry_value_set_by_key(event, "breadcrumbs", breadcrumbs); } - // event <- global: ["global1", "event3", "global4", "event5"] + // event <- isolation: ["global1", "event3", "global4", "event5"] sentry__scope_apply_to_event( - global_scope, options, event, SENTRY_SCOPE_BREADCRUMBS); + isolation_scope, options, event, SENTRY_SCOPE_BREADCRUMBS); sentry_value_t result = sentry_value_get_by_key(event, "breadcrumbs"); TEST_CHECK(sentry_value_get_type(result) == SENTRY_VALUE_TYPE_LIST); @@ -1051,12 +1182,22 @@ SENTRY_TEST(scope_breadcrumbs) sentry_value_decref(event); } - SENTRY_WITH_SCOPE (global_scope) { + SENTRY_WITH_SCOPE (isolation_scope) { // local: ["local2", "local6"] sentry_scope_t *local_scope = sentry_local_scope_new(); + TEST_CHECK_PTR_EQUAL(local_scope->parent, isolation_scope); sentry_scope_add_breadcrumb(local_scope, breadcrumb_ts("local2", 2)); sentry_scope_add_breadcrumb(local_scope, breadcrumb_ts("local6", 6)); + sentry_value_t breadcrumbs + = sentry__scope_breadcrumbs_to_list(local_scope); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(breadcrumbs), 4); + TEST_CHECK_MESSAGE_EQUAL(breadcrumbs, 0, "global1"); + TEST_CHECK_MESSAGE_EQUAL(breadcrumbs, 1, "local2"); + TEST_CHECK_MESSAGE_EQUAL(breadcrumbs, 2, "global4"); + TEST_CHECK_MESSAGE_EQUAL(breadcrumbs, 3, "local6"); + sentry_value_decref(breadcrumbs); + // event: ["event3", "event5"] sentry_value_t event = sentry_value_new_object(); { @@ -1066,21 +1207,25 @@ SENTRY_TEST(scope_breadcrumbs) sentry_value_set_by_key(event, "breadcrumbs", breadcrumbs); } - // event <- local: ["local2", "event3", "event5", "local6"] - sentry__scope_apply_to_event( - local_scope, options, event, SENTRY_SCOPE_BREADCRUMBS); + // event <- isolation: ["global1", "event3", "global4", "event5"] + sentry_value_t isolation_event = sentry__value_clone(event); + sentry__scope_apply_to_event(isolation_scope, options, isolation_event, + SENTRY_SCOPE_BREADCRUMBS); - sentry_value_t result = sentry_value_get_by_key(event, "breadcrumbs"); + sentry_value_t result + = sentry_value_get_by_key(isolation_event, "breadcrumbs"); TEST_CHECK(sentry_value_get_type(result) == SENTRY_VALUE_TYPE_LIST); TEST_CHECK_INT_EQUAL(sentry_value_get_length(result), 4); - TEST_CHECK_MESSAGE_EQUAL(result, 0, "local2"); + TEST_CHECK_MESSAGE_EQUAL(result, 0, "global1"); TEST_CHECK_MESSAGE_EQUAL(result, 1, "event3"); - TEST_CHECK_MESSAGE_EQUAL(result, 2, "event5"); - TEST_CHECK_MESSAGE_EQUAL(result, 3, "local6"); + TEST_CHECK_MESSAGE_EQUAL(result, 2, "global4"); + TEST_CHECK_MESSAGE_EQUAL(result, 3, "event5"); + sentry_value_decref(isolation_event); - // event <- global: ["local2", "event3", "global4", "event5", "local6"] + // event <- local and isolation: + // ["local2", "event3", "global4", "event5", "local6"] sentry__scope_apply_to_event( - global_scope, options, event, SENTRY_SCOPE_BREADCRUMBS); + local_scope, options, event, SENTRY_SCOPE_BREADCRUMBS); result = sentry_value_get_by_key(event, "breadcrumbs"); TEST_CHECK(sentry_value_get_type(result) == SENTRY_VALUE_TYPE_LIST); @@ -1441,6 +1586,10 @@ SENTRY_TEST(scope_local_attributes) sentry_value_t local_attributes = sentry__scope_load_attributes(local_scope); + TEST_CHECK_JSON_VALUE( + sentry_value_get_by_key(local_attributes, "global"), + "{\"type\":\"string\",\"value\":\"global\"}"); + // Verify local attributes are set TEST_CHECK_STRING_EQUAL( sentry_value_as_string(sentry_value_get_by_key( @@ -1594,6 +1743,20 @@ SENTRY_TEST(scope_transaction) TEST_CHECK_STRING_EQUAL( sentry_value_as_string(transaction), "my-transaction"); sentry_value_decref(transaction); + + sentry_scope_t *local_scope = sentry_scope_new(); + for (size_t i = 0; i < 3; i++) { + if (i == 1) { + sentry_scope_set_transaction(local_scope, "local-transaction"); + } else if (i == 2) { + sentry_scope_clear(local_scope); + } + transaction = sentry__scope_ref_transaction(local_scope); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(transaction), + i == 1 ? "local-transaction" : "my-transaction"); + sentry_value_decref(transaction); + } + sentry_scope_free(local_scope); } sentry_close(); @@ -2144,7 +2307,7 @@ SENTRY_TEST(scope_observer_deferred_flush) observer_data.total_flush_count = 0; observer_data.nested_flush_count = 0; - sentry_scope_t *scope = sentry__scope_getref(); + sentry_scope_t *scope = sentry_scope_acquire(); TEST_ASSERT(sentry_scope_begin_write(scope) == 0); sentry_scope_set_tag(scope, "batched", "value"); TEST_CHECK_INT_EQUAL(observer_data.nested_flush_count, 0); @@ -2157,7 +2320,7 @@ SENTRY_TEST(scope_observer_deferred_flush) TEST_ASSERT(sentry_scope_begin_read(scope) == 0); sentry_scope_end_read(scope); TEST_CHECK_INT_EQUAL(observer_data.total_flush_count, 1); - sentry__scope_finish(scope); + sentry_scope_release(scope); sentry_close(); } @@ -2345,6 +2508,7 @@ SENTRY_TEST(scope_observer_user) TEST_CHECK(d.was_called); TEST_CHECK_JSON_VALUE(d.user, "{}"); + sentry_value_decref(d.user); sentry_close(); } @@ -2502,10 +2666,10 @@ SENTRY_TEST(scope_tags_flush) } TEST_CHECK_INT_EQUAL(flush_data.total_flush_count, 3); - sentry_scope_t *scope = sentry__scope_getref(); + sentry_scope_t *scope = sentry_scope_acquire(); sentry_scope_set_tag(scope, "direct", "value"); TEST_CHECK_INT_EQUAL(flush_data.total_flush_count, 3); - sentry__scope_finish_mut(scope); + sentry_scope_release(scope); TEST_CHECK_INT_EQUAL(flush_data.total_flush_count, 4); sentry_value_decref(observer_data.tags); @@ -2906,6 +3070,10 @@ SENTRY_TEST(scope_clone_preserves_data) SENTRY_TEST(scope_attachments) { + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_add_attachment(options, "configured.txt"); + TEST_ASSERT_INT_EQUAL(sentry_init(options), 0); + sentry_scope_t *scope = sentry_scope_new(); sentry_uuid_t first_id = sentry_scope_add_attachment( scope, sentry_attachment_from_bytes("first", 5, "first.txt")); @@ -2915,30 +3083,33 @@ SENTRY_TEST(scope_attachments) TEST_CHECK(!sentry_uuid_is_nil(&second_id)); sentry_value_t snapshot = sentry__scope_load_attachments(scope); - TEST_CHECK_INT_EQUAL(sentry_value_get_length(snapshot), 2); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(snapshot), 3); TEST_CHECK_STRING_EQUAL( sentry__attachment_get_filename(sentry_value_get_by_index(snapshot, 0)), "first.txt"); TEST_CHECK_STRING_EQUAL( sentry__attachment_get_filename(sentry_value_get_by_index(snapshot, 1)), "second.txt"); + TEST_CHECK_STRING_EQUAL( + sentry__attachment_get_filename(sentry_value_get_by_index(snapshot, 2)), + "configured.txt"); sentry_uuid_t third_id = sentry_scope_add_attachment( scope, sentry_attachment_from_bytes("third", 5, "third.txt")); TEST_CHECK(!sentry_uuid_is_nil(&third_id)); sentry_value_t current = sentry__scope_load_attachments(scope); - TEST_CHECK_INT_EQUAL(sentry_value_get_length(current), 3); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(current), 4); TEST_CHECK_STRING_EQUAL( sentry__attachment_get_filename(sentry_value_get_by_index(current, 2)), "third.txt"); sentry_value_decref(current); - TEST_CHECK_INT_EQUAL(sentry_value_get_length(snapshot), 2); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(snapshot), 3); sentry_scope_remove_attachment(scope, first_id); current = sentry__scope_load_attachments(scope); - TEST_CHECK_INT_EQUAL(sentry_value_get_length(current), 2); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(current), 3); TEST_CHECK_STRING_EQUAL( sentry__attachment_get_filename(sentry_value_get_by_index(current, 0)), "second.txt"); @@ -2946,22 +3117,26 @@ SENTRY_TEST(scope_attachments) sentry__attachment_get_filename(sentry_value_get_by_index(current, 1)), "third.txt"); sentry_value_decref(current); - TEST_CHECK_INT_EQUAL(sentry_value_get_length(snapshot), 2); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(snapshot), 3); TEST_CHECK_STRING_EQUAL( sentry__attachment_get_filename(sentry_value_get_by_index(snapshot, 0)), "first.txt"); sentry_scope_clear(scope); current = sentry__scope_load_attachments(scope); - TEST_CHECK_INT_EQUAL(sentry_value_get_length(current), 0); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(current), 1); + TEST_CHECK_STRING_EQUAL( + sentry__attachment_get_filename(sentry_value_get_by_index(current, 0)), + "configured.txt"); sentry_value_decref(current); - TEST_CHECK_INT_EQUAL(sentry_value_get_length(snapshot), 2); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(snapshot), 3); TEST_CHECK_STRING_EQUAL( sentry__attachment_get_filename(sentry_value_get_by_index(snapshot, 1)), "second.txt"); sentry_value_decref(snapshot); sentry_scope_free(scope); + sentry_close(); } SENTRY_TEST(scope_clone_shares_span) @@ -3093,7 +3268,10 @@ SENTRY_TEST(scope_clear) // Everything is reset to the state of a fresh scope. TEST_CHECK(scope_value_get_length(sentry__scope_load_tags, scope) == 0); TEST_CHECK(scope_value_get_length(sentry__scope_load_extra, scope) == 0); - TEST_CHECK(scope_value_get_length(sentry__scope_load_contexts, scope) == 0); + sentry_value_t contexts = sentry__scope_load_contexts(scope); + TEST_CHECK(sentry_value_is_null(sentry_value_get_by_key(contexts, "ctx"))); + TEST_CHECK(!sentry_value_is_null(sentry_value_get_by_key(contexts, "os"))); + sentry_value_decref(contexts); TEST_CHECK( scope_value_get_length(sentry__scope_load_attributes, scope) == 0); scope_user = sentry__scope_ref_user(scope); @@ -3475,10 +3653,25 @@ SENTRY_TEST(scope_bind_span_or_transaction_not_both) TEST_ASSERT(!!span); // Only one binding can be active at a time. + sentry_scope_t *parent = sentry_scope_acquire(); + sentry_scope_set_transaction_object(parent, tx); sentry_scope_t *scope = sentry_scope_new(); + sentry_transaction_t *inherited + = sentry__scope_ref_transaction_object(scope); + TEST_CHECK_PTR_EQUAL(inherited, tx); + sentry__transaction_decref(inherited); + sentry_value_t value = sentry__scope_load_span_or_transaction(scope); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(value, "span_id")), + sentry_value_as_string(sentry_value_get_by_key(tx->inner, "span_id"))); + sentry_value_decref(value); + sentry_scope_set_span(parent, span); + sentry_span_t *bound_span = sentry__scope_ref_span(scope); + TEST_CHECK_PTR_EQUAL(bound_span, span); + sentry__span_decref(bound_span); sentry_scope_set_span(scope, span); sentry_scope_set_transaction_object(scope, tx); - sentry_span_t *bound_span = sentry__scope_ref_span(scope); + bound_span = sentry__scope_ref_span(scope); sentry_transaction_t *bound_tx = sentry__scope_ref_transaction_object(scope); TEST_CHECK(bound_span == NULL); @@ -3486,11 +3679,18 @@ SENTRY_TEST(scope_bind_span_or_transaction_not_both) sentry__span_decref(bound_span); sentry__transaction_decref(bound_tx); + sentry_scope_set_transaction_object(parent, tx); sentry_scope_set_span(scope, span); bound_tx = sentry__scope_ref_transaction_object(scope); bound_span = sentry__scope_ref_span(scope); TEST_CHECK(bound_tx == NULL); TEST_CHECK_PTR_EQUAL(bound_span, span); + value = sentry__scope_load_span_or_transaction(scope); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(value, "span_id")), + sentry_value_as_string( + sentry_value_get_by_key(span->inner, "span_id"))); + sentry_value_decref(value); sentry__transaction_decref(bound_tx); sentry__span_decref(bound_span); @@ -3499,11 +3699,13 @@ SENTRY_TEST(scope_bind_span_or_transaction_not_both) bound_span = sentry__scope_ref_span(scope); bound_tx = sentry__scope_ref_transaction_object(scope); TEST_CHECK(bound_span == NULL); - TEST_CHECK(bound_tx == NULL); + TEST_CHECK_PTR_EQUAL(bound_tx, tx); + TEST_CHECK(!sentry__scope_remove_transaction_object(scope, tx)); sentry__span_decref(bound_span); sentry__transaction_decref(bound_tx); sentry_scope_free(scope); + sentry_scope_release(parent); sentry_span_finish(span); sentry_transaction_finish(tx); @@ -3636,45 +3838,53 @@ SENTRY_TEST(scope_read_blocks_write) { sentry_scope_t *scope = sentry_scope_new(); TEST_ASSERT(!!scope); - sentry_scope_set_environment(scope, "old"); - sentry_scope_set_tag(scope, "version", "old"); - - scope_access_thread_t state = { .scope = scope }; - sentry__waitable_flag_init(&state.started); - sentry__waitable_flag_init(&state.finished); - - TEST_ASSERT(sentry_scope_begin_read(scope) == 0); - sentry_value_t environment = sentry__scope_ref_environment(scope); - TEST_CHECK_STRING_EQUAL(sentry_value_as_string(environment), "old"); - sentry_value_decref(environment); - - sentry_threadid_t thread; - sentry__thread_init(&thread); - TEST_ASSERT( - !sentry__thread_spawn(&thread, write_scope_during_read, &state)); - TEST_ASSERT(sentry__waitable_flag_wait(&state.started, 1000)); - TEST_CHECK(!sentry__waitable_flag_wait(&state.finished, 10)); - - TEST_ASSERT(sentry_scope_begin_read(scope) == 0); - sentry_value_t tag - = scope_value_get_by_key(sentry__scope_load_tags, scope, "version"); - TEST_CHECK_STRING_EQUAL(sentry_value_as_string(tag), "old"); - sentry_value_decref(tag); - TEST_CHECK(sentry_scope_begin_write(scope) == 1); - sentry_scope_end_read(scope); - TEST_CHECK(!sentry__waitable_flag_wait(&state.finished, 10)); - - sentry_scope_end_read(scope); - TEST_CHECK(sentry__waitable_flag_wait(&state.finished, 1000)); - sentry__thread_join(thread); + sentry_scope_t *parent = sentry_scope_acquire(); + TEST_ASSERT(!!parent); + sentry_scope_t *targets[] = { scope, parent }; + for (size_t i = 0; i < 2; i++) { + sentry_scope_t *target = targets[i]; + sentry_scope_clear(scope); + sentry_scope_set_environment(target, "old"); + sentry_scope_set_tag(target, "version", "old"); + + scope_access_thread_t state = { .scope = target }; + sentry__waitable_flag_init(&state.started); + sentry__waitable_flag_init(&state.finished); + + TEST_ASSERT(sentry_scope_begin_read(scope) == 0); + sentry_value_t environment = sentry__scope_ref_environment(scope); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(environment), "old"); + sentry_value_decref(environment); - TEST_CHECK_INT_EQUAL(state.result, 0); - environment = sentry__scope_ref_environment(scope); - tag = scope_value_get_by_key(sentry__scope_load_tags, scope, "version"); - TEST_CHECK_STRING_EQUAL(sentry_value_as_string(environment), "new"); - TEST_CHECK_STRING_EQUAL(sentry_value_as_string(tag), "new"); - sentry_value_decref(environment); - sentry_value_decref(tag); + sentry_threadid_t thread; + sentry__thread_init(&thread); + TEST_ASSERT( + !sentry__thread_spawn(&thread, write_scope_during_read, &state)); + TEST_ASSERT(sentry__waitable_flag_wait(&state.started, 1000)); + TEST_CHECK(!sentry__waitable_flag_wait(&state.finished, 10)); + + TEST_ASSERT(sentry_scope_begin_read(scope) == 0); + sentry_value_t tag + = scope_value_get_by_key(sentry__scope_load_tags, scope, "version"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(tag), "old"); + sentry_value_decref(tag); + TEST_CHECK(sentry_scope_begin_write(target) == 1); + sentry_scope_end_read(scope); + TEST_CHECK(!sentry__waitable_flag_wait(&state.finished, 10)); + + sentry_scope_end_read(scope); + TEST_CHECK(sentry__waitable_flag_wait(&state.finished, 1000)); + sentry__thread_join(thread); + + TEST_CHECK_INT_EQUAL(state.result, 0); + environment = sentry__scope_ref_environment(scope); + tag = scope_value_get_by_key(sentry__scope_load_tags, scope, "version"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(environment), "new"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(tag), "new"); + sentry_value_decref(environment); + sentry_value_decref(tag); + } + sentry_scope_release(parent); sentry_scope_free(scope); } @@ -3706,3 +3916,464 @@ SENTRY_TEST(scope_access_multiple) sentry_scope_free(first); sentry_scope_free(second); } + +SENTRY_TEST(scope_access_pool) +{ + sentry_scope_t *scope = sentry_scope_acquire(); + TEST_ASSERT(!!scope); + + TEST_ASSERT(sentry_scope_begin_read(scope) == 0); + sentry_scope_end_read(scope); + sentry_scope_release(scope); + + sentry_scope_t *first = sentry_scope_new(); + sentry_scope_t *second = sentry_scope_new(); + sentry_scope_t *third = sentry_scope_new(); + TEST_ASSERT(!!first && !!second && !!third); + + TEST_ASSERT(sentry_scope_begin_read(first) == 0); + TEST_ASSERT(sentry_scope_begin_read(second) == 0); + TEST_ASSERT(sentry_scope_begin_read(third) == 0); + + sentry_scope_end_read(second); + sentry_scope_end_read(first); + sentry_scope_end_read(third); + + TEST_ASSERT(sentry_scope_begin_read(third) == 0); + sentry_scope_end_read(third); + + sentry_scope_free(first); + sentry_scope_free(second); + sentry_scope_free(third); +} + +typedef struct { + scope_access_thread_t access; + sentry_threadid_t thread; + int started; + bool finished; +} breadcrumb_read_t; + +static sentry_value_t +before_breadcrumb_read_scope(sentry_value_t breadcrumb, void *data) +{ + breadcrumb_read_t *state = data; + state->started = sentry__thread_spawn( + &state->thread, read_scope_batch, &state->access); + state->finished = state->started == 0 + && sentry__waitable_flag_wait(&state->access.finished, 1000); + return breadcrumb; +} + +SENTRY_TEST(before_breadcrumb_read_scope) +{ + breadcrumb_read_t state = { 0 }; + sentry__waitable_flag_init(&state.access.started); + sentry__waitable_flag_init(&state.access.finished); + sentry__thread_init(&state.thread); + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_before_breadcrumb( + options, before_breadcrumb_read_scope, &state); + TEST_ASSERT_INT_EQUAL(sentry_init(options), 0); + state.access.scope = sentry_scope_acquire(); + TEST_ASSERT(!!state.access.scope); + + sentry_add_breadcrumb(sentry_value_new_breadcrumb(NULL, "read scope")); + TEST_CHECK_INT_EQUAL(state.started, 0); + TEST_CHECK(state.finished); + if (state.started == 0) { + sentry__thread_join(state.thread); + } + sentry__thread_free(&state.thread); + sentry_value_decref(state.access.environment); + sentry_value_decref(state.access.tag); + sentry_scope_release(state.access.scope); + sentry_close(); +} + +SENTRY_TEST(scope_isolation_reset) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_release(options, "configured-release"); + sentry_options_set_environment(options, "configured-environment"); + sentry_options_set_sdk_name(options, "sentry.native.test"); + sentry_options_set_max_breadcrumbs(options, 2); + TEST_ASSERT_INT_EQUAL(sentry_init(options), 0); + + sentry_scope_t *scope = sentry_scope_acquire(); + TEST_ASSERT(!!scope); + sentry_scope_set_release(scope, "changed-release"); + sentry_scope_set_environment(scope, "changed-environment"); + sentry_scope_set_tag(scope, "tag", "value"); + sentry_scope_set_user(scope, sentry_value_new_user("42", NULL, NULL, NULL)); + + sentry_scope_clear(scope); + + sentry_value_t release = sentry__scope_ref_release(scope); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(release), "configured-release"); + sentry_value_decref(release); + sentry_value_t environment = sentry__scope_ref_environment(scope); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(environment), "configured-environment"); + sentry_value_decref(environment); + sentry_value_t sdk = sentry__scope_ref_client_sdk(scope); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(sdk, "name")), + "sentry.native.test"); + sentry_value_decref(sdk); + sentry_value_t user = sentry__scope_ref_user(scope); + TEST_CHECK(sentry_value_is_null(user)); + sentry_value_decref(user); + TEST_CHECK_INT_EQUAL( + scope_value_get_length(sentry__scope_load_tags, scope), 0); + + for (size_t i = 0; i < 3; i++) { + sentry_scope_add_breadcrumb( + scope, sentry_value_new_breadcrumb(NULL, "breadcrumb")); + } + TEST_CHECK_INT_EQUAL(scope_breadcrumb_count(scope), 2); + + sentry_scope_release(scope); + sentry_close(); +} + +typedef struct { + sentry_scope_t *scope; + sentry_value_t event; +} shared_scope_thread_t; + +SENTRY_THREAD_FN +capture_shared_scope(void *data) +{ + shared_scope_thread_t *state = data; + state->scope = sentry_scope_acquire(); + sentry_set_tag("thread", "worker"); + state->event = sentry_value_new_event(); + SENTRY_WITH_OPTIONS (options) { + sentry__scope_apply_to_event( + state->scope, options, state->event, SENTRY_SCOPE_NONE); + } + return 0; +} + +SENTRY_TEST(scope_isolation_shared) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_auto_session_tracking(options, false); + TEST_ASSERT_INT_EQUAL(sentry_init(options), 0); + sentry_scope_t *scope = sentry_scope_acquire(); + TEST_ASSERT(!!scope); + shared_scope_thread_t state = { 0 }; + sentry_threadid_t thread; + sentry__thread_init(&thread); + TEST_ASSERT(!sentry__thread_spawn(&thread, capture_shared_scope, &state)); + sentry__thread_join(thread); + sentry__thread_free(&thread); + TEST_CHECK_PTR_EQUAL(state.scope, scope); + sentry_value_t tag + = scope_value_get_by_key(sentry__scope_load_tags, scope, "thread"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(tag), "worker"); + sentry_value_decref(tag); + TEST_CHECK(!sentry_value_is_null(sentry_value_get_by_key( + sentry_value_get_by_key(state.event, "contexts"), "os"))); + TEST_CHECK( + !sentry_value_is_null(sentry_value_get_by_key(state.event, "sdk"))); + sentry_value_decref(state.event); + sentry_scope_release(state.scope); + sentry_scope_release(scope); + sentry_close(); +} + +SENTRY_TEST(scope_global_immutable) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_auto_session_tracking(options, false); + TEST_ASSERT_INT_EQUAL(sentry_init(options), 0); + sentry_scope_t *scope = sentry_scope_acquire(); + const sentry_scope_t *global = scope->parent; + TEST_ASSERT(!!global); + TEST_CHECK(global->parent == NULL); + TEST_CHECK(global->state == NULL); + TEST_CHECK(global->flush_func == NULL); + TEST_CHECK_INT_EQUAL(global->num_observers, 0); + TEST_CHECK(scope->num_observers > 0); + TEST_CHECK(scope->flush_func != NULL); + sentry_scope_t *clone = sentry_scope_clone(scope); + TEST_ASSERT(!!clone); + sentry_value_t sdk = sentry__scope_ref_client_sdk(global); + sentry_value_t os + = scope_value_get_by_key(sentry__scope_load_contexts, global, "os"); + TEST_CHECK(sentry_value_is_frozen(sdk)); + char *sdk_json = sentry_value_to_json(sdk); + char *os_json = sentry_value_to_json(os); + TEST_CHECK_INT_EQUAL(sentry_value_set_by_key( + sdk, "name", sentry_value_new_string("changed")), + 1); + sentry_value_set_by_key(os, "name", sentry_value_new_string("changed")); + sentry_value_decref(sdk); + sentry_value_decref(os); + + sentry_set_context("os", sentry_value_new_object()); + sentry_scope_clear(scope); + sentry_value_t event = sentry_value_new_event(); + sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_NONE); + TEST_CHECK_JSON_VALUE(sentry_value_get_by_key( + sentry_value_get_by_key(event, "contexts"), "os"), + os_json); + sentry_value_decref(event); + sentry_close(); + + SENTRY_TEST_OPTIONS_NEW(next_options); + sentry_options_set_auto_session_tracking(next_options, false); + TEST_ASSERT_INT_EQUAL(sentry_init(next_options), 0); + sentry_scope_t *next = sentry_scope_acquire(); + TEST_CHECK(next != scope); + TEST_CHECK_PTR_EQUAL(next->parent, global); + sdk = sentry__scope_ref_client_sdk(global); + os = scope_value_get_by_key(sentry__scope_load_contexts, global, "os"); + TEST_CHECK_JSON_VALUE(sdk, sdk_json); + TEST_CHECK_JSON_VALUE(os, os_json); + sentry_value_decref(sdk); + sentry_value_decref(os); + sentry_scope_release(scope); + event = sentry_value_new_event(); + sentry__scope_apply_to_event(clone, next_options, event, SENTRY_SCOPE_NONE); + TEST_CHECK_JSON_VALUE(sentry_value_get_by_key(event, "sdk"), sdk_json); + TEST_CHECK_JSON_VALUE(sentry_value_get_by_key( + sentry_value_get_by_key(event, "contexts"), "os"), + os_json); + sentry_value_decref(event); + sentry_scope_free(clone); + sentry_free(sdk_json); + sentry_free(os_json); + sentry_scope_release(next); + sentry_close(); +} + +SENTRY_TEST(scope_sdk_inheritance) +{ + sentry_scope_t *before_init = sentry_scope_acquire(); + TEST_ASSERT(!!before_init); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_auto_session_tracking(options, false); + sentry_options_set_sdk_name(options, "sentry.native.test"); + sentry_integration_t *integration = SENTRY_MAKE(sentry_integration_t); + TEST_ASSERT(!!integration); + integration->name = "test-integration"; + TEST_ASSERT(sentry__options_add_integration(options, integration)); + TEST_ASSERT_INT_EQUAL(sentry_init(options), 0); + sentry_scope_release(before_init); + + sentry_scope_t *scope = sentry_scope_acquire(); + sentry_value_t sdk = sentry__scope_ref_client_sdk(scope); + sentry_value_t global_sdk = sentry__scope_ref_client_sdk(scope->parent); + TEST_CHECK_JSON_VALUE( + sentry_value_get_by_key(global_sdk, "name"), "\"sentry.native.test\""); + TEST_CHECK(sentry_value_is_frozen(global_sdk)); + char *json = sentry_value_to_json(global_sdk); + TEST_CHECK_JSON_VALUE(sdk, json); + sentry_value_t integrations = sentry_value_get_by_key(sdk, "integrations"); + bool found = false; + for (size_t i = 0; i < sentry_value_get_length(integrations); i++) { + found |= !strcmp( + sentry_value_as_string(sentry_value_get_by_index(integrations, i)), + "test-integration"); + } + TEST_CHECK(found); + sentry_value_decref(sdk); + sentry_value_decref(global_sdk); + + sentry_scope_clear(scope); + sdk = sentry__scope_ref_client_sdk(scope); + TEST_CHECK_JSON_VALUE(sdk, json); + sentry_value_decref(sdk); + sentry_scope_t *local = sentry_scope_new(); + sdk = sentry__scope_ref_client_sdk(local); + TEST_CHECK_JSON_VALUE(sdk, json); + sentry_value_decref(sdk); + sentry_scope_free(local); + + sentry_value_t event = sentry_value_new_event(); + sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_NONE); + TEST_CHECK_JSON_VALUE(sentry_value_get_by_key(event, "sdk"), json); + sentry_value_decref(event); + sentry_free(json); + sentry_scope_release(scope); + sentry_close(); +} + +SENTRY_TEST(scope_parent_lifetime) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_auto_session_tracking(options, false); + TEST_ASSERT_INT_EQUAL(sentry_init(options), 0); + sentry_set_tag("parent", "original"); + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + sentry_scope_t *clone = sentry_scope_clone(scope); + TEST_ASSERT(!!clone); + sentry_scope_free(scope); + sentry_close(); + + SENTRY_TEST_OPTIONS_NEW(next_options); + sentry_options_set_auto_session_tracking(next_options, false); + TEST_ASSERT_INT_EQUAL(sentry_init(next_options), 0); + sentry_set_tag("parent", "new"); + sentry_value_t event = sentry_value_new_event(); + sentry__scope_apply_to_event(clone, next_options, event, SENTRY_SCOPE_NONE); + TEST_CHECK_JSON_VALUE(sentry_value_get_by_key( + sentry_value_get_by_key(event, "tags"), "parent"), + "\"original\""); + sentry_value_decref(event); + sentry_scope_free(clone); + sentry_close(); +} + +SENTRY_TEST(scope_global_before_init) +{ + sentry_scope_t *scope = sentry_scope_acquire(); + TEST_ASSERT(!!scope); + const sentry_scope_t *global = scope->parent; + TEST_ASSERT(!!global); + sentry_value_t sdk = sentry__scope_ref_client_sdk(global); + TEST_ASSERT(sentry_value_get_type(sdk) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK(sentry_value_is_frozen(sdk)); + TEST_CHECK( + sentry_value_get_length(sentry_value_get_by_key(sdk, "name")) > 0); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(sdk, "version")), + SENTRY_SDK_VERSION); + sentry_value_decref(sdk); + sentry_value_t contexts = sentry__scope_load_contexts(global); + TEST_CHECK(sentry_value_get_type(contexts) == SENTRY_VALUE_TYPE_OBJECT); + sentry_value_decref(contexts); + sentry_scope_release(scope); + sentry_close(); +} + +SENTRY_TEST(scope_defaults) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_release(options, "configured-release"); + sentry_options_set_environment(options, "configured-environment"); + sentry_options_set_traces_sample_rate(options, 0.25); + TEST_ASSERT_INT_EQUAL(sentry_init(options), 0); + sentry_scope_t *scope = sentry_scope_acquire(); + TEST_ASSERT(!!scope); + sentry_scope_t *local = sentry_scope_new(); + TEST_ASSERT(!!local); + + for (int i = 0; i < 4; i++) { + if (i == 1 || i == 3) { + sentry_set_release("override-release"); + sentry_set_environment("override-environment"); + } + if (i == 2) { + sentry_set_release(NULL); + sentry_set_environment(NULL); + } else if (i == 3) { + sentry_scope_clear(scope); + } + const char *release + = i == 1 ? "override-release" : "configured-release"; + const char *environment + = i == 1 ? "override-environment" : "configured-environment"; + sentry_value_t event = sentry_value_new_event(); + sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_NONE); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(event, "release")), + release); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + event, "environment")), + environment); + sentry_value_decref(event); + sentry_value_t dsc = sentry__scope_load_dsc(scope); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(dsc, "release")), + release); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(dsc, "environment")), + environment); + sentry_value_decref(dsc); + dsc = sentry__scope_load_dsc(local); + TEST_CHECK( + sentry_value_as_double(sentry_value_get_by_key(dsc, "sample_rate")) + == 0.25); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(dsc, "release")), + release); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(dsc, "environment")), + environment); + sentry_value_decref(dsc); + sentry_value_t global_release + = sentry__scope_ref_release(scope->parent); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(global_release), "configured-release"); + sentry_value_decref(global_release); + sentry_value_t global_environment + = sentry__scope_ref_environment(scope->parent); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(global_environment), + "configured-environment"); + sentry_value_decref(global_environment); + } + sentry_scope_set_release(local, "local-release"); + sentry_value_t dsc = sentry__scope_load_dsc(local); + TEST_CHECK_JSON_VALUE( + sentry_value_get_by_key(dsc, "release"), "\"local-release\""); + sentry_value_decref(dsc); + dsc = sentry_value_new_object(); + sentry_value_set_by_key( + dsc, "release", sentry_value_new_string("upstream")); + sentry__scope_freeze_dsc(scope, dsc); + sentry_value_decref(dsc); + dsc = sentry__scope_load_dsc(local); + TEST_CHECK_JSON_VALUE(dsc, "{\"release\":\"upstream\"}"); + sentry_value_decref(dsc); + sentry_scope_free(local); + sentry_scope_release(scope); + sentry_close(); +} + +SENTRY_TEST(scope_parent_lock) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + sentry_scope_t *parent = sentry_scope_acquire(); + TEST_ASSERT(!!parent); + sentry_scope_set_environment(parent, "old"); + sentry_scope_set_tag(parent, "version", "old"); + + scope_access_thread_t state = { .scope = parent }; + sentry__waitable_flag_init(&state.started); + sentry__waitable_flag_init(&state.finished); + + TEST_ASSERT(sentry_scope_begin_write(scope) == 0); + sentry_threadid_t thread; + sentry__thread_init(&thread); + TEST_ASSERT( + !sentry__thread_spawn(&thread, write_scope_during_read, &state)); + TEST_ASSERT(sentry__waitable_flag_wait(&state.started, 1000)); + TEST_CHECK(!sentry__waitable_flag_wait(&state.finished, 10)); + + sentry_value_t environment = sentry__scope_ref_environment(scope); + sentry_value_t tag + = scope_value_get_by_key(sentry__scope_load_tags, scope, "version"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(environment), "old"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(tag), "old"); + sentry_value_decref(environment); + sentry_value_decref(tag); + + sentry_scope_end_write(scope); + TEST_CHECK(sentry__waitable_flag_wait(&state.finished, 1000)); + sentry__thread_join(thread); + sentry__thread_free(&thread); + TEST_CHECK_INT_EQUAL(state.result, 0); + environment = sentry__scope_ref_environment(scope); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(environment), "new"); + sentry_value_decref(environment); + sentry_scope_release(parent); + sentry_scope_free(scope); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 1dad9f1253..75c3d1ea7c 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -73,6 +73,7 @@ XX(batcher_sync_flush_sends) XX(before_breadcrumb_discard) XX(before_breadcrumb_modify) XX(before_breadcrumb_passthrough) +XX(before_breadcrumb_read_scope) XX(bgworker_delayed_cleanup) XX(bgworker_delayed_current) XX(bgworker_delayed_drop_current) @@ -384,6 +385,7 @@ XX(sampling_before_send) XX(sampling_decision) XX(sampling_transaction) XX(scope_access_multiple) +XX(scope_access_pool) XX(scope_attachments) XX(scope_bind_span) XX(scope_bind_span_or_transaction_not_both) @@ -406,11 +408,16 @@ XX(scope_clone_keeps_bound_span) XX(scope_clone_preserves_data) XX(scope_clone_shares_span) XX(scope_contexts) +XX(scope_defaults) XX(scope_environment) XX(scope_extra) XX(scope_fingerprint) XX(scope_fingerprint_n) XX(scope_global_attributes) +XX(scope_global_before_init) +XX(scope_global_immutable) +XX(scope_isolation_reset) +XX(scope_isolation_shared) XX(scope_last_event_id) XX(scope_level) XX(scope_local_attributes) @@ -431,6 +438,8 @@ XX(scope_observer_tags) XX(scope_observer_transaction) XX(scope_observer_user) XX(scope_ownership) +XX(scope_parent_lifetime) +XX(scope_parent_lock) XX(scope_propagation_context) XX(scope_read_blocks_write) XX(scope_read_shared) @@ -438,6 +447,7 @@ XX(scope_rebind_same_object) XX(scope_release) XX(scope_remove_fingerprint_capture) XX(scope_restore_trace) +XX(scope_sdk_inheritance) XX(scope_set_attribute_invalid_decref_value) XX(scope_set_attribute_null_key_decref_value) XX(scope_tags) From 5ae31e0eb313f7e53d6c8c3688461dab5ec3da75 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 15 Oct 2026 17:44:51 +0200 Subject: [PATCH 12/23] feat(native): Provide crash metadata to `on_crash` (#2174) Provide exception metadata, registers, and a basic crashing-thread stack for crash identification in native `on_crash`, matching `inproc`. Use the same payload for `before_send` when `on_crash` is unset. - Capture up to 128 fault-context IPs and unwind source in shared memory. Retain the faulting IP when available. - Linux: SDK `libunwind` with signal-safe logging. - macOS: SDK frame-pointer walker, guarded Mach reads, PAC stripping, `__DARWIN_OPAQUE_ARM_THREAD_STATE64` accessors, and `instruction_addr_adjustment=none`. - Android/Linux without `libunwind`: bounded frame-pointer capture via `process_vm_readv`; avoids `libunwindstack`'s C++ allocation. - Windows: MSVC x64 `RtlLookupFunctionEntry`/`RtlVirtualUnwind` walking without DbgHelp; SP progress permits recursive frames. Frame-pointer capture on x86/ARM64 and other x64 builds. - ARM32 frame-pointer fallbacks retain only the faulting IP. The daemon reuses partial captures and enriches existing frames. Preserve exception/register/frame edits and removals, and honor callback rejection. --- CHANGELOG.md | 1 + CMakeLists.txt | 2 +- include/sentry.h | 4 +- src/CMakeLists.txt | 6 + src/backends/native/sentry_crash_context.h | 117 +++++++++- src/backends/native/sentry_crash_daemon.c | 231 ++++++++----------- src/backends/native/sentry_crash_handler.c | 211 +++++++++++++++-- src/backends/native/sentry_wer.c | 1 + src/backends/sentry_backend_native.c | 13 ++ src/sentry_os.c | 28 +++ src/sentry_os.h | 4 + src/sentry_telemetry.c | 2 - src/unwinder/sentry_unwinder_dbghelp.c | 85 +++++++ src/unwinder/sentry_unwinder_libunwind.c | 7 - src/unwinder/sentry_unwinder_libunwind_mac.c | 42 +++- tests/assertions.py | 16 +- tests/fixtures/integration.c | 21 +- tests/test_integration_crashpad.py | 7 +- tests/test_integration_native.py | 84 ++++++- tests/test_integration_stdout.py | 21 +- tests/unit/CMakeLists.txt | 3 + 21 files changed, 697 insertions(+), 209 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1a66ea7a0..3954b3afd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Add `sentry_scope_begin_read`/`sentry_scope_end_read` and `sentry_scope_begin_write`/`sentry_scope_end_write` for consistent reads of multiple scope properties and applying multiple scope changes with a single scope flush. ([#2107](https://github.com/getsentry/sentry-native/pull/2107)) - Add `sentry_scope_acquire` and `sentry_scope_release` to access the isolation scope. ([#2160](https://github.com/getsentry/sentry-native/pull/2160)) +- Native: Provide exception metadata, registers, and a basic crashing-thread stack to `on_crash` for crash identification. ([#2174](https://github.com/getsentry/sentry-native/pull/2174)) ## Unreleased diff --git a/CMakeLists.txt b/CMakeLists.txt index 752a1325e2..29647c24c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1164,7 +1164,7 @@ if(SENTRY_BUILD_TESTS) else() # Disable all optimizations for the `sentry_test_integration` in gcc/clang. This allows us to keep crash triggers simple. # The effects besides reproducible code-gen across compiler versions, will be negligible for build- and runtime. - target_compile_options(sentry_test_integration PRIVATE $) + target_compile_options(sentry_test_integration PRIVATE $) endif() if(CMAKE_GENERATOR STREQUAL "Xcode") diff --git a/include/sentry.h b/include/sentry.h index d5b0e93eba..0909ac42d4 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1609,7 +1609,7 @@ SENTRY_API void sentry_options_set_before_send( * The hint is always provided and can be used to modify attachments on the * event. * - * Only the `inproc` backend currently fills the passed-in event with crash + * Only the `inproc` and `native` backends fill the passed-in event with crash * meta-data. Since both `breakpad` and `crashpad` use minidumps to capture the * crash state, the passed-in event is empty when using these backends. Changes * to the event from inside the hooks will be passed along, but in the case of @@ -1632,7 +1632,7 @@ SENTRY_API void sentry_options_set_before_send( * - if you are not interested in normal events, but only want to act on * crashes (within the limits mentioned below), then only define an * `on_crash` callback with the option to filter (on all backends) or enrich - * (only inproc) the crash event + * (inproc and native) the crash event * * This function may be invoked inside a signal handler and must be safe for * that purpose, see https://man7.org/linux/man-pages/man7/signal-safety.7.html. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b528429f98..61286232ad 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -264,6 +264,12 @@ if(WIN32) sentry_target_sources_cwd(sentry unwinder/sentry_unwinder_dbghelp.c ) + if(MINGW AND CMAKE_C_COMPILER_ID STREQUAL "Clang") + set_source_files_properties( + "${CMAKE_CURRENT_SOURCE_DIR}/unwinder/sentry_unwinder_dbghelp.c" + DIRECTORY "${PROJECT_SOURCE_DIR}" + PROPERTIES COMPILE_OPTIONS -fms-extensions) + endif() endif() if(PROSPERO) diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index 14678564e1..5e73b61921 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -9,7 +9,10 @@ #include "sentry.h" // For sentry_minidump_mode_t #include "sentry_boot.h" +#include "sentry_os.h" #include "sentry_sync.h" +#include "sentry_utils.h" +#include "sentry_value.h" #include #include @@ -39,6 +42,14 @@ typedef DWORD pid_t; #define SENTRY_CRASH_MAX_THREADS 256 #define SENTRY_CRASH_MAX_MODULES 2048 #define SENTRY_CRASH_MAX_MAPPINGS 4096 +// Max frames for pre-captured backtrace (crash handler -> daemon) +#define SENTRY_CRASH_MAX_BACKTRACE_FRAMES 128 + +typedef struct { + uint32_t count; + uint8_t frame_pointer; + uint64_t ips[SENTRY_CRASH_MAX_BACKTRACE_FRAMES]; +} sentry_crash_capture_t; // Max path length in crash context // Use system PATH_MAX where available (typically 4096 on Linux/macOS, 260 on @@ -149,9 +160,6 @@ typedef struct { #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) -// Max frames for pre-captured backtrace (signal handler -> daemon) -# define SENTRY_CRASH_MAX_BACKTRACE_FRAMES 128 - /** * Linux/Android thread context */ @@ -170,12 +178,6 @@ typedef struct { siginfo_t siginfo; ucontext_t context; - // Pre-captured backtrace from signal handler using libunwind (DWARF-based). - // This works without frame pointers, unlike the daemon's FP-based walking. - // The daemon prefers this over FP-walking when backtrace_count > 0. - size_t backtrace_count; - uint64_t backtrace_ips[SENTRY_CRASH_MAX_BACKTRACE_FRAMES]; - // Additional thread contexts (for multi-thread dumps) size_t num_threads; sentry_thread_context_linux_t threads[SENTRY_CRASH_MAX_THREADS]; @@ -323,6 +325,11 @@ typedef struct { sentry_crash_platform_windows_t platform; #endif + // Fault-context capture, callee-first; failed capture may contain the IP. + // On Linux, libunwind captures using DWARF without frame pointers. + // The daemon prefers captures over its own stack walk. + sentry_crash_capture_t capture; + // Sentry-specific metadata paths char database_path[SENTRY_CRASH_MAX_PATH]; // Shared across runs char run_path[SENTRY_CRASH_MAX_PATH]; // For current run @@ -374,4 +381,96 @@ sentry__crash_context_init(sentry_crash_context_t *ctx) // Add 8KB padding for safety and future additions #define SENTRY_CRASH_SHM_SIZE (sizeof(sentry_crash_context_t) + (8 * 1024)) +static inline sentry_value_t +sentry__crash_context_build_stacktrace( + const sentry_crash_context_t *ctx, sentry_value_t registers) +{ + const sentry_crash_capture_t *capture = &ctx->capture; + size_t count = capture->count; + if (count == 0) { + sentry_value_decref(registers); + return sentry_value_new_null(); + } + if (count > SENTRY_CRASH_MAX_BACKTRACE_FRAMES) { + count = SENTRY_CRASH_MAX_BACKTRACE_FRAMES; + } + sentry_value_t stacktrace = sentry_value_new_object(); + sentry_value_t frames = sentry_value_new_list(); + // Sentry expects frames in reverse order (outermost caller first) + for (size_t i = count; i > 0; i--) { + sentry_value_t frame = sentry_value_new_object(); + sentry_value_set_by_key(frame, "instruction_addr", + sentry__value_new_addr(capture->ips[i - 1])); + // Trust describes the unwind source, not the emitted frame index. + sentry_value_set_by_key(frame, "trust", + sentry_value_new_string(i == 1 ? "context" + : capture->frame_pointer ? "fp" + : "cfi")); + sentry_value_append(frames, frame); + } + sentry_value_set_by_key(stacktrace, "frames", frames); + sentry_value_set_by_key(stacktrace, "registers", registers); +#if defined(SENTRY_PLATFORM_MACOS) + // the SDK's macOS unwinder already adjusts return addresses + sentry_value_set_by_key(stacktrace, "instruction_addr_adjustment", + sentry_value_new_string("none")); +#endif + return stacktrace; +} + +static inline sentry_value_t +sentry__crash_context_build_exception(const sentry_crash_context_t *ctx, + const char *mechanism_type, bool handled, sentry_value_t stacktrace) +{ + // Build exception +#if defined(SENTRY_PLATFORM_UNIX) + int signal_number = ctx->platform.signum; + const char *signal_name = sentry__signal_to_string(signal_number); +#elif defined(SENTRY_PLATFORM_WINDOWS) + // Exception code is used directly below as unsigned + const char *signal_name = "EXCEPTION"; +#else +# error Unsupported platform +#endif + + // Avoid snprintf in signal handlers (not async-signal-safe) + char value_buf[128] = "Fatal crash: "; + size_t prefix_len = sizeof("Fatal crash: ") - 1; + size_t len = MIN(strlen(signal_name), sizeof(value_buf) - prefix_len - 1); + memcpy(value_buf + prefix_len, signal_name, len); + value_buf[prefix_len + len] = '\0'; + sentry_value_t exc = sentry_value_new_exception(signal_name, value_buf); + + // Add mechanism + sentry_value_t mechanism = sentry_value_new_object(); + sentry_value_set_by_key( + mechanism, "type", sentry_value_new_string(mechanism_type)); + sentry_value_set_by_key( + mechanism, "synthetic", sentry_value_new_bool(true)); + sentry_value_set_by_key( + mechanism, "handled", sentry_value_new_bool(handled)); + // Add signal metadata + sentry_value_t meta = sentry_value_new_object(); + sentry_value_t signal_info = sentry_value_new_object(); +#if defined(SENTRY_PLATFORM_WINDOWS) + // Windows exception codes are unsigned 32-bit values (e.g., 0xC0000005) + // Use uint64 to preserve the unsigned value for the symbolicator + sentry_value_set_by_key(signal_info, "number", + sentry_value_new_uint64((uint64_t)ctx->platform.exception_code)); +#else + sentry_value_set_by_key( + signal_info, "number", sentry_value_new_int32(signal_number)); +#endif + sentry_value_set_by_key( + signal_info, "name", sentry_value_new_string(signal_name)); + sentry_value_set_by_key(meta, "signal", signal_info); + sentry_value_set_by_key(mechanism, "meta", meta); + sentry_value_set_by_key(exc, "mechanism", mechanism); + // Add stacktrace to exception + if (!sentry_value_is_null(stacktrace)) { + sentry_value_set_by_key(exc, "stacktrace", stacktrace); + } + return exc; +} + #endif diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 65a70b85f5..d69b2de5f4 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -500,34 +500,6 @@ add_attachment_refs(sentry_envelope_t *envelope, sentry_value_decref(list); } -#if defined(SENTRY_PLATFORM_UNIX) -/** - * Get signal name from signal number (Unix platforms only) - */ -static const char * -get_signal_name(int signum) -{ - switch (signum) { - case SIGABRT: - return "SIGABRT"; - case SIGBUS: - return "SIGBUS"; - case SIGFPE: - return "SIGFPE"; - case SIGILL: - return "SIGILL"; - case SIGSEGV: - return "SIGSEGV"; - case SIGSYS: - return "SIGSYS"; - case SIGTRAP: - return "SIGTRAP"; - default: - return "UNKNOWN"; - } -} -#endif - /** * Build registers value from crash context for a specific thread. * @@ -717,8 +689,41 @@ enrich_frame_with_module_info( || defined(SENTRY_PLATFORM_MACOS) static void enrich_frame_with_symbol( const sentry_crash_context_t *ctx, sentry_value_t frame, uint64_t addr); +#elif defined(SENTRY_PLATFORM_WINDOWS) +static void +enrich_frame_with_symbol( + const sentry_crash_context_t *ctx, sentry_value_t frame, uint64_t addr) +{ + (void)ctx; + if (!g_stack_walk_process) { + return; + } + const size_t size = sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(WCHAR); + SYMBOL_INFOW *symbol = sentry_malloc(size); + if (!symbol) { + return; + } + memset(symbol, 0, size); + symbol->SizeOfStruct = sizeof(SYMBOL_INFOW); + symbol->MaxNameLen = MAX_SYM_NAME; + if (SymFromAddrW(g_stack_walk_process, addr, NULL, symbol) + && !(symbol->Flags & SYMFLAG_EXPORT)) { + char *name = sentry__string_from_wstr(symbol->Name); + if (name) { + sentry_value_set_by_key( + frame, "function", sentry_value_new_string(name)); + sentry_free(name); + } + sentry_value_set_by_key( + frame, "symbol_addr", sentry__value_new_addr(symbol->Address)); + } + sentry_free(symbol); +} #endif +static void enrich_stacktrace( + const sentry_crash_context_t *ctx, sentry_value_t stacktrace); + /** * Build stacktrace frames for a specific thread using frame pointer-based * unwinding. Reads the captured stack memory and walks the frame chain. @@ -732,6 +737,15 @@ static sentry_value_t build_stacktrace_for_thread( const sentry_crash_context_t *ctx, size_t thread_idx) { + if (thread_idx == SIZE_MAX + && (ctx->capture.count > 1 + || (ctx->capture.count == 1 && ctx->capture.ips[0] != 0))) { + sentry_value_t captured = sentry__crash_context_build_stacktrace( + ctx, build_registers_from_ctx(ctx, SIZE_MAX)); + enrich_stacktrace(ctx, captured); + return captured; + } + sentry_value_t stacktrace = sentry_value_new_object(); sentry_value_t frames = sentry_value_new_list(); @@ -1080,57 +1094,6 @@ build_stacktrace_for_thread( // unwinding failed #endif -#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) - // Fallback: use pre-captured libunwind backtrace if available - // (DWARF-based, works without frame pointers). - if (ctx->platform.backtrace_count > 0 - && (thread_idx == SIZE_MAX || thread_idx == 0)) { - SENTRY_DEBUGF("Using pre-captured libunwind backtrace (%zu frames)", - ctx->platform.backtrace_count); - - for (size_t i = 0; - i < ctx->platform.backtrace_count && frame_count < MAX_STACK_FRAMES; - i++) { - uint64_t frame_ip = ctx->platform.backtrace_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)); - // Trust describes the unwind source, not the emitted frame index. - // If the initial cursor frame is filtered out, the next emitted - // frame was still reached via CFI. - 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); - enrich_frame_with_symbol(ctx, temp_frames[frame_count], frame_ip); - frame_count++; - } - - if (stack_buf) { - sentry_free(stack_buf); - } - - if (frame_count == 0) { - sentry_value_decref(frames); - sentry_value_decref(stacktrace); - return sentry_value_new_null(); - } - - // Sentry expects frames in reverse order (outermost caller first) - 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)); - return stacktrace; - } -#endif - // Add the crashing frame (instruction pointer) if (ip != 0 && is_valid_code_addr(ip)) { temp_frames[frame_count] = sentry_value_new_object(); @@ -3205,6 +3168,40 @@ apply_wer_context(sentry_value_t event, const sentry_crash_context_t *ctx) } #endif +static void +enrich_stacktrace(const sentry_crash_context_t *ctx, sentry_value_t stacktrace) +{ + sentry_value_t frames = sentry_value_get_by_key(stacktrace, "frames"); + if (sentry_value_get_type(frames) != SENTRY_VALUE_TYPE_LIST) { + return; + } + for (size_t i = 0; i < sentry_value_get_length(frames); i++) { + sentry_value_t frame = sentry_value_get_by_index(frames, i); + sentry_value_t value + = sentry_value_get_by_key(frame, "instruction_addr"); + uint64_t addr = sentry_value_as_uint64(value); + if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_STRING) { + const char *text = sentry_value_as_string(value); + char *end = NULL; + addr = strtoull(text, &end, 0); + if (end == text || *end != '\0') { + continue; + } + } + if (!addr) { + continue; + } + if (sentry_value_is_null(sentry_value_get_by_key(frame, "package"))) { + enrich_frame_with_module_info(ctx, frame, addr); + } + if (sentry_value_is_null(sentry_value_get_by_key(frame, "function")) + && sentry_value_is_null( + sentry_value_get_by_key(frame, "symbol_addr"))) { + enrich_frame_with_symbol(ctx, frame, addr); + } + } +} + /** * Build a native event and set the level, mechanism, and handled state * @@ -3253,53 +3250,7 @@ build_native_event(const sentry_crash_context_t *ctx, sentry_value_set_by_key(event, "level", sentry_value_new_string(level)); - // Build exception -#if defined(SENTRY_PLATFORM_UNIX) - int signal_number = ctx->platform.signum; - const char *signal_name = get_signal_name(signal_number); -#elif defined(SENTRY_PLATFORM_WINDOWS) - // Exception code is used directly below as unsigned - const char *signal_name = "EXCEPTION"; -#else -# error Unsupported platform -#endif - - sentry_value_t exc = sentry_value_new_object(); - sentry_value_set_by_key(exc, "type", sentry_value_new_string(signal_name)); - - char value_buf[128]; - snprintf(value_buf, sizeof(value_buf), "Fatal crash: %s", signal_name); - sentry_value_set_by_key(exc, "value", sentry_value_new_string(value_buf)); - - // Add mechanism - sentry_value_t mechanism = sentry_value_new_object(); - sentry_value_set_by_key( - mechanism, "type", sentry_value_new_string(mechanism_type)); - sentry_value_set_by_key( - mechanism, "synthetic", sentry_value_new_bool(true)); - sentry_value_set_by_key( - mechanism, "handled", sentry_value_new_bool(handled)); - - // Add signal metadata - sentry_value_t meta = sentry_value_new_object(); - sentry_value_t signal_info = sentry_value_new_object(); -#if defined(SENTRY_PLATFORM_WINDOWS) - // Windows exception codes are unsigned 32-bit values (e.g., 0xC0000005) - // Use uint64 to preserve the unsigned value for the symbolicator - sentry_value_set_by_key(signal_info, "number", - sentry_value_new_uint64((uint64_t)ctx->platform.exception_code)); -#else - sentry_value_set_by_key( - signal_info, "number", sentry_value_new_int32(signal_number)); -#endif - sentry_value_set_by_key( - signal_info, "name", sentry_value_new_string(signal_name)); - sentry_value_set_by_key(meta, "signal", signal_info); - sentry_value_set_by_key(mechanism, "meta", meta); - - sentry_value_set_by_key(exc, "mechanism", mechanism); - - // Add stacktrace to exception + // other threads and missing local captures may still need DbgHelp #if defined(SENTRY_PLATFORM_WINDOWS) g_stack_walk_process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, @@ -3317,14 +3268,24 @@ build_native_event(const sentry_crash_context_t *ctx, ctx->crashed_pid, GetLastError()); } #endif - sentry_value_set_by_key(exc, "stacktrace", build_stacktrace_from_ctx(ctx)); - // Wrap exception in values array - sentry_value_t exceptions = sentry_value_new_object(); - sentry_value_t exc_values = sentry_value_new_list(); - sentry_value_append(exc_values, exc); - sentry_value_set_by_key(exceptions, "values", exc_values); - sentry_value_set_by_key(event, "exception", exceptions); + sentry_value_t exception = sentry_value_get_by_key(event, "exception"); + sentry_value_t values = sentry_value_get_by_key(exception, "values"); + sentry_value_t exc = sentry_value_get_by_index(values, 0); + if (sentry_value_is_null(exc)) { + sentry_value_t stacktrace = build_stacktrace_from_ctx(ctx); + if (sentry_value_is_null(stacktrace)) { + stacktrace = sentry__crash_context_build_stacktrace( + ctx, build_registers_from_ctx(ctx, SIZE_MAX)); + } + exc = sentry__crash_context_build_exception( + ctx, mechanism_type, handled, stacktrace); + // Wrap exception in values array + sentry_event_add_exception(event, exc); + } else { + sentry_value_t stacktrace = sentry_value_get_by_key(exc, "stacktrace"); + enrich_stacktrace(ctx, stacktrace); + } // Always add threads with names to the event JSON { @@ -4058,6 +4019,10 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) // Mark as processing sentry__atomic_store(&ctx->state, SENTRY_CRASH_STATE_PROCESSING); SENTRY_DEBUG("Marked state as PROCESSING"); + if (!ctx->event_path[0]) { + SENTRY_DEBUG("Crash event discarded by local callback"); + goto done; + } // Check crash reporting mode int mode = ctx->crash_reporting_mode; diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index a811712a04..af52044678 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -24,6 +24,9 @@ # include # include # include +# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +# include +# endif # include #elif defined(SENTRY_PLATFORM_WINDOWS) # include @@ -31,6 +34,25 @@ # include #endif +#if (defined(SENTRY_PLATFORM_WINDOWS) \ + && !((defined(_MSC_VER) || defined(__clang__)) \ + && (defined(_M_AMD64) || defined(_M_ARM64)))) \ + || defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +static inline bool +capture_valid_ip(uint64_t ip) +{ + if (ip < 0x1000) { + return false; + } +# if defined(__x86_64__) || defined(_M_AMD64) + if (ip > 0x00007FFFFFFFFFFFULL) { + return false; + } +# endif + return true; +} +#endif + // signal_safe_memcpy and signal_safe_memzero are only used on Unix // (for signal handler context copying) #if defined(SENTRY_PLATFORM_UNIX) @@ -285,6 +307,86 @@ safe_build_stack_path( } # endif +# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +// libunwindstack uses C++ allocation; keep the handler's FP walk bounded +static void +capture_frame_pointer_stack( + sentry_crash_capture_t *capture, const ucontext_t *uctx) +{ +# if defined(SYS_process_vm_readv) && !defined(__arm__) + uintptr_t sp = sentry__ucontext_get_sp(uctx); + uintptr_t fp = 0; +# if defined(__x86_64__) + fp = (uintptr_t)uctx->uc_mcontext.gregs[REG_RBP]; +# elif defined(__i386__) + fp = (uintptr_t)uctx->uc_mcontext.gregs[REG_EBP]; +# elif defined(__aarch64__) + fp = (uintptr_t)uctx->uc_mcontext.regs[29]; +# endif + capture->frame_pointer = true; + while (fp && fp >= sp && fp - sp <= SENTRY_CRASH_MAX_STACK_CAPTURE + && fp % sizeof(uintptr_t) == 0 + && capture->count < SENTRY_CRASH_MAX_BACKTRACE_FRAMES) { + uintptr_t pair[2]; + struct iovec local = { pair, sizeof(pair) }; + struct iovec remote = { (void *)fp, sizeof(pair) }; + if (syscall(SYS_process_vm_readv, getpid(), &local, 1, &remote, 1, 0) + != (ssize_t)sizeof(pair)) { + break; + } + uintptr_t next = pair[0]; + uintptr_t ret = pair[1]; + if (!ret) { + break; + } + if (!capture_valid_ip(ret) + || (next + && (next <= fp || next < sp + || next - sp > SENTRY_CRASH_MAX_STACK_CAPTURE))) { + break; + } + capture->ips[capture->count++] = (uint64_t)ret; + if (!next) { + break; + } + fp = next; + } +# else + (void)capture; + (void)uctx; +# endif +} +# endif + +# if defined(SENTRY_WITH_UNWINDER_LIBUNWIND) \ + || defined(SENTRY_WITH_UNWINDER_LIBUNWIND_MAC) +static void +capture_stack(sentry_crash_capture_t *capture, ucontext_t *uctx) +{ +# if defined(SENTRY_WITH_UNWINDER_LIBUNWIND) + // the libunwind path stops at IP zero; recover callers via FP + if (capture->ips[0] == 0) { + capture_frame_pointer_stack(capture, uctx); + return; + } +# endif + sentry_ucontext_t context = { 0 }; + context.user_context = uctx; + void *ips[SENTRY_CRASH_MAX_BACKTRACE_FRAMES]; + size_t count = sentry_unwind_stack_from_ucontext( + &context, ips, SENTRY_CRASH_MAX_BACKTRACE_FRAMES); +# if defined(SENTRY_WITH_UNWINDER_LIBUNWIND_MAC) + capture->frame_pointer = true; +# endif + for (size_t i = capture->count; i < count; i++) { + capture->ips[i] = (uint64_t)(uintptr_t)ips[i]; + } + if (count > capture->count) { + capture->count = (uint32_t)count; + } +} +# endif + /** * Signal handler (signal-safe) */ @@ -315,8 +417,8 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) # endif sentry_crash_ipc_t *ipc = g_crash_ipc; - if (!ipc || !ipc->shmem) { - // No IPC available, forward to the previous handler + if (!uctx || !ipc || !ipc->shmem) { + // No context or IPC available, forward to the previous handler // Re-enable previous signal handlers before re-raising to prevent loops reset_signal_handlers(); if (g_handler_strategy != SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { @@ -341,6 +443,19 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) // Fill crash context ctx->crashed_pid = getpid(); ctx->crashed_tid = get_tid(); + ctx->capture.count = 0; + ctx->capture.frame_pointer = false; +# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + uintptr_t fault_ip = sentry__ucontext_get_ip(uctx); +# elif defined(SENTRY_PLATFORM_MACOS) && defined(__x86_64__) + uintptr_t fault_ip = uctx->uc_mcontext->__ss.__rip; +# elif defined(SENTRY_PLATFORM_MACOS) && defined(__aarch64__) + uintptr_t fault_ip + = SENTRY__ARM64_GET_PC(uctx->uc_mcontext->__ss) & 0x00007FFFFFFFFFFFULL; +# else + uintptr_t fault_ip = 0; +# endif + ctx->capture.ips[ctx->capture.count++] = (uint64_t)fault_ip; # if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) ctx->platform.signum = signum; @@ -380,29 +495,16 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) // libunwind's core unwinding API (unw_init_local2, unw_step, unw_get_reg) // is async-signal-safe. The daemon will prefer this over FP-based walking. # ifdef SENTRY_WITH_UNWINDER_LIBUNWIND - ctx->platform.backtrace_count = 0; - { - unw_cursor_t cursor; - int ret = unw_init_local2( - &cursor, (unw_context_t *)uctx, UNW_INIT_SIGNAL_FRAME); - if (ret == 0) { - size_t n = 0; - while (n < SENTRY_CRASH_MAX_BACKTRACE_FRAMES) { - unw_word_t ip = 0; - if (unw_get_reg(&cursor, UNW_REG_IP, &ip) < 0 || ip == 0) { - break; - } - ctx->platform.backtrace_ips[n++] = (uint64_t)ip; - if (unw_step(&cursor) <= 0) { - break; - } - } - ctx->platform.backtrace_count = n; - } - } + capture_stack(&ctx->capture, uctx); +# else + capture_frame_pointer_stack(&ctx->capture, uctx); # endif # elif defined(SENTRY_PLATFORM_MACOS) +# if defined(SENTRY_WITH_UNWINDER_LIBUNWIND_MAC) + capture_stack(&ctx->capture, uctx); +# endif + // Capture all threads (signal-safe on macOS) ctx->platform.num_threads = 0; task_t task = mach_task_self(); @@ -1009,6 +1111,69 @@ static LPTOP_LEVEL_EXCEPTION_FILTER g_previous_filter = NULL; static LONG WINAPI crash_exception_filter(EXCEPTION_POINTERS *exception_info); +static void +capture_windows_stack(sentry_crash_capture_t *capture, const CONTEXT *original) +{ + capture->count = 0; + capture->frame_pointer = false; +# if (defined(_MSC_VER) || defined(__clang__)) \ + && (defined(_M_AMD64) || defined(_M_ARM64)) + sentry_ucontext_t uctx = { 0 }; + uctx.exception_ptrs.ContextRecord = (CONTEXT *)original; + void *ips[SENTRY_CRASH_MAX_BACKTRACE_FRAMES]; + capture->count = (uint32_t)sentry_unwind_stack_from_ucontext( + &uctx, ips, SENTRY_CRASH_MAX_BACKTRACE_FRAMES); + for (size_t i = 0; i < capture->count; i++) { + capture->ips[i] = (uint64_t)(uintptr_t)ips[i]; + } +# elif defined(_M_AMD64) || defined(_M_IX86) || defined(_M_ARM64) +# if defined(_M_AMD64) + uintptr_t ip = original->Rip; + uintptr_t fp = original->Rbp; + uintptr_t sp = original->Rsp; +# elif defined(_M_IX86) + uintptr_t ip = original->Eip; + uintptr_t fp = original->Ebp; + uintptr_t sp = original->Esp; +# else + uintptr_t ip = original->Pc; + uintptr_t fp = original->Fp; + uintptr_t sp = original->Sp; +# endif + capture->ips[capture->count++] = ip; + capture->frame_pointer = true; + while (fp && fp >= sp && fp - sp <= SENTRY_CRASH_MAX_STACK_CAPTURE + && fp % sizeof(uintptr_t) == 0 + && capture->count < SENTRY_CRASH_MAX_BACKTRACE_FRAMES) { + uintptr_t pair[2]; + SIZE_T read = 0; + if (!ReadProcessMemory( + GetCurrentProcess(), (void *)fp, pair, sizeof(pair), &read) + || read != sizeof(pair)) { + break; + } + if (!pair[1]) { + break; + } + if (!capture_valid_ip(pair[1]) + || (pair[0] + && (pair[0] <= fp || pair[0] < sp + || pair[0] - sp > SENTRY_CRASH_MAX_STACK_CAPTURE))) { + break; + } + capture->ips[capture->count++] = pair[1]; + if (!pair[0]) { + break; + } + fp = pair[0]; + } +# elif defined(_M_ARM) + capture->ips[capture->count++] = original->Pc; +# else + (void)original; +# endif +} + static void crash_sigabrt_handler(EXCEPTION_POINTERS *exception_pointers) { @@ -1069,6 +1234,8 @@ crash_exception_filter(EXCEPTION_POINTERS *exception_info) sentry__crash_ipc_notify(ipc); } + capture_windows_stack(&ctx->capture, exception_info->ContextRecord); + // Call Sentry's exception handler sentry_ucontext_t sentry_uctx = { 0 }; sentry_uctx.exception_ptrs = *exception_info; diff --git a/src/backends/native/sentry_wer.c b/src/backends/native/sentry_wer.c index f0050ee5df..d4c8b63511 100644 --- a/src/backends/native/sentry_wer.c +++ b/src/backends/native/sentry_wer.c @@ -154,6 +154,7 @@ process_wer_exception( == SENTRY_CRASH_STATE_READY) { ctx->crashed_pid = GetProcessId(exception_info->hProcess); ctx->crashed_tid = GetThreadId(exception_info->hThread); + ctx->capture.count = 0; ctx->platform.exception_code = exception_info->exceptionRecord.ExceptionCode; ctx->platform.exception_record = exception_info->exceptionRecord; diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index dcea80d18c..4b3c01c0ff 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -1277,6 +1277,7 @@ native_backend_flush_scope( // Create event with current scope sentry_value_t event = sentry_value_new_object(); + sentry__ensure_event_id(event, NULL); sentry_value_set_by_key( event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); @@ -1379,6 +1380,15 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) sentry_value_t event = sentry_value_new_event(); sentry_value_set_by_key( event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); + sentry_crash_context_t *ctx + = state && state->ipc ? state->ipc->shmem : NULL; + if (ctx) { + sentry_value_t exc = sentry__crash_context_build_exception(ctx, + "signalhandler", false, + sentry__crash_context_build_stacktrace( + ctx, sentry__build_registers(uctx))); + sentry_event_add_exception(event, exc); + } sentry_hint_t hint; sentry__hint_init(&hint); @@ -1501,6 +1511,9 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) sentry_value_decref(event); sentry_value_decref(transaction); } + if (!should_handle && ctx) { + ctx->event_path[0] = '\0'; + } sentry__hint_deinit(&hint); } } diff --git a/src/sentry_os.c b/src/sentry_os.c index 47c2ffe835..69b2390fd8 100644 --- a/src/sentry_os.c +++ b/src/sentry_os.c @@ -1235,3 +1235,31 @@ sentry__build_registers(const sentry_ucontext_t *uctx) return registers; } + +#if defined(SENTRY_PLATFORM_UNIX) +/** + * Get signal name from signal number (Unix platforms only) + */ +const char * +sentry__signal_to_string(int signum) +{ + switch (signum) { + case SIGABRT: + return "SIGABRT"; + case SIGBUS: + return "SIGBUS"; + case SIGFPE: + return "SIGFPE"; + case SIGILL: + return "SIGILL"; + case SIGSEGV: + return "SIGSEGV"; + case SIGSYS: + return "SIGSYS"; + case SIGTRAP: + return "SIGTRAP"; + default: + return "UNKNOWN"; + } +} +#endif diff --git a/src/sentry_os.h b/src/sentry_os.h index 3dd68fcedc..6be9dcb434 100644 --- a/src/sentry_os.h +++ b/src/sentry_os.h @@ -72,4 +72,8 @@ sentry_value_t sentry__get_os_context(void); sentry_value_t sentry__build_registers(const sentry_ucontext_t *uctx); +#if defined(SENTRY_PLATFORM_UNIX) +const char *sentry__signal_to_string(int signum); +#endif + #endif diff --git a/src/sentry_telemetry.c b/src/sentry_telemetry.c index 2bcecedd6a..5e7602fc4f 100644 --- a/src/sentry_telemetry.c +++ b/src/sentry_telemetry.c @@ -69,8 +69,6 @@ sentry__telemetry_force_flush(void) void sentry__telemetry_flush_crash_safe(void) { - SENTRY_SIGNAL_SAFE_LOG("DEBUG crash-safe telemetry flush"); sentry__logs_flush_crash_safe(); sentry__metrics_flush_crash_safe(); - SENTRY_SIGNAL_SAFE_LOG("DEBUG crash-safe telemetry flush complete"); } diff --git a/src/unwinder/sentry_unwinder_dbghelp.c b/src/unwinder/sentry_unwinder_dbghelp.c index 82c344e3bf..f5e6fe5e1a 100644 --- a/src/unwinder/sentry_unwinder_dbghelp.c +++ b/src/unwinder/sentry_unwinder_dbghelp.c @@ -10,6 +10,7 @@ typedef WORD(NTAPI *RtlCaptureStackBackTraceProc)(DWORD FramesToSkip, #ifdef __clang__ # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wmissing-prototypes" +# pragma clang diagnostic ignored "-Wlanguage-extension-token" #endif size_t @@ -41,6 +42,90 @@ sentry__unwind_stack_dbghelp( #endif } +#if (defined(_MSC_VER) || defined(__clang__)) \ + && (defined(_M_AMD64) || defined(_M_ARM64)) + if (!addr) { + // unwind tables without DbgHelp or symbol loading + CONTEXT walk = *uctx->exception_ptrs.ContextRecord; +# if defined(_M_ARM64) + DWORD64 *ip = &walk.Pc; + DWORD64 *sp = &walk.Sp; +# else + DWORD64 *ip = &walk.Rip; + DWORD64 *sp = &walk.Rsp; +# endif + size_t size = 0; + if (size < max_frames) { + ptrs[size++] = (void *)(uintptr_t)*ip; + } else { + return 0; + } + while (size < max_frames) { + DWORD64 control_pc = *ip; +# if defined(_M_ARM64) + // use the call instruction for lookup, keeping frame PCs unadjusted + if (walk.ContextFlags & CONTEXT_UNWOUND_TO_CALL) { + control_pc -= 4; + } + DWORD64 previous_ip = *ip; +# endif + DWORD64 image_base = 0; + PRUNTIME_FUNCTION entry = NULL; + __try { + entry = RtlLookupFunctionEntry(control_pc, &image_base, NULL); + } __except (EXCEPTION_EXECUTE_HANDLER) { + break; + } + DWORD64 previous_sp = *sp; + if (entry) { + PVOID handler_data = NULL; + DWORD64 establisher = 0; + __try { + RtlVirtualUnwind(UNW_FLAG_NHANDLER, image_base, control_pc, + entry, &walk, &handler_data, &establisher, NULL); + } __except (EXCEPTION_EXECUTE_HANDLER) { + break; + } + } else { +# if defined(_M_ARM64) + walk.Pc = walk.Lr; + walk.ContextFlags |= CONTEXT_UNWOUND_TO_CALL; +# else + DWORD64 ret = 0; + SIZE_T read = 0; + if (!ReadProcessMemory(GetCurrentProcess(), (void *)walk.Rsp, + &ret, sizeof(ret), &read) + || read != sizeof(ret)) { + break; + } + walk.Rip = ret; + walk.Rsp += sizeof(ret); +# endif + } + if (!*ip) { + break; + } + if (*ip < 0x1000 +# if defined(_M_AMD64) + || *ip > 0x00007FFFFFFFFFFFULL +# endif + ) { + break; + } +# if defined(_M_ARM64) + if (*sp < previous_sp + || (*sp == previous_sp && *ip == previous_ip)) { +# else + if (*sp <= previous_sp) { +# endif + break; + } + ptrs[size++] = (void *)(uintptr_t)*ip; + } + return size; + } +#endif + sentry__init_dbghelp(); CONTEXT ctx = *uctx->exception_ptrs.ContextRecord; diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 9a154608c8..883a85622f 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -1,5 +1,4 @@ #include "sentry_boot.h" -#include "sentry_logger.h" #include "sentry_unwinder.h" #define UNW_LOCAL_ONLY #include @@ -156,7 +155,6 @@ sentry__unwind_stack_libunwind( int ret = unw_init_local2(&cursor, (unw_context_t *)uctx->user_context, UNW_INIT_SIGNAL_FRAME); if (ret != 0) { - SENTRY_WARN("Failed to initialize libunwind with ucontext"); return 0; } } else { @@ -171,13 +169,11 @@ sentry__unwind_stack_libunwind( # pragma clang diagnostic pop #endif if (ret != 0) { - SENTRY_WARN("Failed to retrieve context with libunwind"); return 0; } ret = unw_init_local(&cursor, &uc); if (ret != 0) { - SENTRY_WARN("Failed to initialize libunwind with local context"); return 0; } } @@ -203,9 +199,6 @@ sentry__unwind_stack_libunwind( // ensure we have a valid stack pointer otherwise we only send the top frame mem_range_t stack = { 0, 0 }; if (uctx && !find_mem_range((uintptr_t)sp, &stack)) { - SENTRY_WARNF("unwinder: SP (%p) is in unmapped memory likely due to " - "stack overflow", - (void *)sp); return n; } diff --git a/src/unwinder/sentry_unwinder_libunwind_mac.c b/src/unwinder/sentry_unwinder_libunwind_mac.c index 668a0bda20..a3c74bcb41 100644 --- a/src/unwinder/sentry_unwinder_libunwind_mac.c +++ b/src/unwinder/sentry_unwinder_libunwind_mac.c @@ -98,6 +98,26 @@ valid_ptr(uintptr_t p, mem_range_t *cache) #endif } +static bool +read_frame(uintptr_t p, uintptr_t record[2], mem_range_t *cache) +{ + if (!valid_ptr(p, cache)) { + return false; + } +#if defined(SENTRY_PLATFORM_MACOS) + mach_vm_size_t size = 0; + return mach_vm_read_overwrite(mach_task_self(), p, 2 * sizeof(uintptr_t), + (mach_vm_address_t)record, &size) + == KERN_SUCCESS + && size == 2 * sizeof(uintptr_t); +#else + const uintptr_t *frame = (uintptr_t *)p; + record[0] = frame[0]; + record[1] = frame[1]; + return true; +#endif +} + /** * This does the same frame-pointer walk for arm64 and x86_64, with the only * difference being which registers value is used as frame-pointer (fp vs rbp) @@ -107,14 +127,14 @@ fp_walk( uintptr_t fp, size_t *n, void **ptrs, size_t max_frames, mem_range_t *cache) { while (*n < max_frames) { - if (!valid_ptr(fp, cache)) { + uintptr_t record[2]; + if (!read_frame(fp, record, cache)) { break; } // arm64 frame record layout: [prev_fp, saved_lr] at fp and fp+8 // x86_64 frame record layout: [prev_rbp, saved_retaddr] at bp and bp+8 - const uintptr_t *record = (uintptr_t *)fp; - const uintptr_t next_fp = record[0]; + const uintptr_t next_fp = STRIP_PAC(record[0]); uintptr_t ret_addr = record[1]; if (!valid_ptr(next_fp, cache) || !ret_addr) { break; @@ -138,20 +158,20 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) #if defined(__arm64__) uintptr_t pc, fp, lr; -# if defined(__arm64e__) - // arm64e uses opaque accessors that handle PAC authentication +# if __DARWIN_OPAQUE_ARM_THREAD_STATE64 + // opaque state uses accessors that handle PAC authentication pc = __darwin_arm_thread_state64_get_pc(mctx->__ss); fp = __darwin_arm_thread_state64_get_fp(mctx->__ss); lr = __darwin_arm_thread_state64_get_lr(mctx->__ss); # else // arm64 can access members directly, strip PAC defensively pc = STRIP_PAC((uintptr_t)mctx->__ss.__pc); - fp = (uintptr_t)mctx->__ss.__fp; + fp = STRIP_PAC((uintptr_t)mctx->__ss.__fp); lr = STRIP_PAC((uintptr_t)mctx->__ss.__lr); # endif // top frame: crash PC points to the faulting instruction, no adjustment - if (pc && n < max_frames) { + if (n < max_frames) { ptrs[n++] = (void *)pc; } @@ -174,11 +194,11 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) // it sub-calls (LR is a stale return within the crashing function). // // In both cases, walking from fp captures the correct remaining frames. - if (valid_ptr(fp, &cache)) { - const uintptr_t *record = (uintptr_t *)fp; + uintptr_t record[2]; + if (read_frame(fp, record, &cache)) { uintptr_t saved_lr = STRIP_PAC(record[1]); if (lr == saved_lr) { - fp_walk(record[0], &n, ptrs, max_frames, &cache); + fp_walk(STRIP_PAC(record[0]), &n, ptrs, max_frames, &cache); } else { fp_walk(fp, &n, ptrs, max_frames, &cache); } @@ -188,7 +208,7 @@ fp_walk_from_uctx(const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) uintptr_t bp = (uintptr_t)mctx->__ss.__rbp; // top frame: no adjustment - if (ip && n < max_frames) { + if (n < max_frames) { ptrs[n++] = (void *)ip; } diff --git a/tests/assertions.py b/tests/assertions.py index 9873cf3aac..ed8c196196 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -556,7 +556,7 @@ def _load_crashpad_attachments(msg): cmake_cache = len(part.get_payload(decode=True)) case "bytes.bin": bytes_bin = part.get_payload(decode=True) - case "callback.txt": + case "before_send.json" | "on_crash.json": callback = part.get_payload(decode=True) if ( @@ -871,3 +871,17 @@ def assert_replay_envelope(envelope, video, replay_id=REPLAY_ID): assert crumbs[2]["category"] == "something else" assert body["replay_video"] == video + + +def assert_crash_stack(event): + assert event["level"] == "fatal" + stack = event["exception"]["values"][0]["stacktrace"] + ips = [int(frame["instruction_addr"], 16) for frame in stack["frames"]] + assert ips and all(ip >= 0x1000 for ip in ips[:-1]) + assert ips[-1] == 0 or ips[-1] >= 0x1000 + registers = stack["registers"] + register = next(key for key in ("rip", "eip", "pc") if key in registers) + assert ips[-1] == int(registers[register], 16) + # ARM32 frame-pointer fallback can provide only the faulting instruction + if "pc" not in registers or "x0" in registers: + assert len(ips) > 1 diff --git a/tests/fixtures/integration.c b/tests/fixtures/integration.c index 5bcfa86d33..ee4c8b5f47 100644 --- a/tests/fixtures/integration.c +++ b/tests/fixtures/integration.c @@ -131,10 +131,11 @@ before_send_callback(sentry_value_t event, sentry_hint_t *hint, void *user_data) { (void)user_data; + char *json = sentry_value_to_json(event); sentry_hint_clear_attachments(hint); sentry_hint_add_attachment(hint, - sentry_attachment_from_bytes( - "before_send", strlen("before_send"), "callback.txt")); + sentry_attachment_from_bytes(json, strlen(json), "before_send.json")); + sentry_free(json); // make our mark on the event sentry_value_set_by_key( @@ -184,10 +185,11 @@ on_crash_callback(const sentry_ucontext_t *uctx, sentry_value_t event, } } + char *json = sentry_value_to_json(event); sentry_hint_clear_attachments(hint); sentry_hint_add_attachment(hint, - sentry_attachment_from_bytes( - "on_crash", strlen("on_crash"), "callback.txt")); + sentry_attachment_from_bytes(json, strlen(json), "on_crash.json")); + sentry_free(json); // tell the backend to retain the event return event; @@ -631,6 +633,14 @@ trigger_oom(void) } } +static void +trigger_null_call(void) +{ + // keep a caller frame independent of libc startup + void (*volatile func)(void) = NULL; + func(); +} + static sentry_value_t create_debug_crumb(const char *message) { @@ -1383,6 +1393,9 @@ main(int argc, char **argv) if (has_arg(argc, argv, "crash")) { sentry_crash(); } + if (has_arg(argc, argv, "crash-null")) { + trigger_null_call(); + } if (has_arg(argc, argv, "stack-overflow")) { trigger_stack_overflow(); } diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index a68f149880..db955c7cfa 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -1,3 +1,4 @@ +import json import os import struct import subprocess @@ -629,8 +630,10 @@ def test_crashpad_dumping_crash(cmake, httpserver, run_args, build_args): callback = next( (arg for arg in run_args if arg in ("before-send", "on-crash")), None ) - expected_callback = callback.replace("-", "_").encode() if callback else None - assert attachments.callback == expected_callback + if callback: + assert json.loads(attachments.callback)["level"] == "fatal" + else: + assert attachments.callback is None event_id = attachments.event["event_id"] if sys.platform == "win32": minidump = tmp_path / ".sentry-native" / "reports" / f"{event_id}.dmp" diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 5eb6d9d39c..37851019dc 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -42,8 +42,9 @@ wait_for, wait_for_file, assert_user_feedback, + assert_crash_stack, ) -from .conditions import has_files +from .conditions import has_files, is_arm64e from .conditions import has_native, has_oom, is_asan, is_tsan, is_qemu, is_wine pytestmark = pytest.mark.skipif( @@ -81,8 +82,22 @@ def test_native_capture_crash(cmake, httpserver): assert_native_crash(envelope) -@pytest.mark.parametrize("callback", ["before-send", "on-crash"]) -def test_native_crash_hint_attachments(cmake, httpserver, callback): +@pytest.mark.parametrize( + "callback, crash", + [ + ("before-send", "crash"), + ("on-crash", "crash"), + pytest.param( + "on-crash", + "crash-null", + marks=pytest.mark.skipif( + sys.platform not in ["linux", "darwin"] or is_wine or is_arm64e, + reason="Null function-pointer calls must fault at IP zero", + ), + ), + ], +) +def test_native_crash_hint_attachments(cmake, httpserver, callback, crash): tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "native"}) httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") @@ -91,7 +106,7 @@ def test_native_crash_hint_attachments(cmake, httpserver, callback): run_crash( tmp_path, "sentry_test_integration", - ["log", "attachment", callback, "crash"], + ["log", "attachment", callback, crash], env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), ) assert waiting.result @@ -101,11 +116,26 @@ def test_native_crash_hint_attachments(cmake, httpserver, callback): assert not any( item.headers.get("filename") == "CMakeCache.txt" for item in envelope ) - assert any( - item.headers.get("filename") == "callback.txt" - and item.payload.bytes == callback.replace("-", "_").encode() - for item in envelope + filename = callback.replace("-", "_") + ".json" + snapshot = json.loads( + next( + item.payload.bytes + for item in envelope + if item.headers.get("filename") == filename + ) ) + assert_crash_stack(snapshot) + if crash == "crash-null": + frames = snapshot["exception"]["values"][0]["stacktrace"]["frames"] + assert int(frames[-1]["instruction_addr"], 16) == 0 + exceptions = envelope.get_event()["exception"]["values"] + assert len(exceptions) == len(snapshot["exception"]["values"]) + exc = exceptions[0] + local_exc = snapshot["exception"]["values"][0] + assert exc["mechanism"] == local_exc["mechanism"] + assert [frame["instruction_addr"] for frame in exc["stacktrace"]["frames"]] == [ + frame["instruction_addr"] for frame in local_exc["stacktrace"]["frames"] + ] def test_native_on_crashed_last_run(cmake, httpserver): @@ -227,7 +257,7 @@ def test_native_wer_crash(cmake, httpserver, crash_arg, exception_code): run_crash( tmp_path, "sentry_test_integration", - ["log", "stdout", crash_arg], + ["log", "stdout", "initial-scope", "no-setup", crash_arg], env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), ) assert waiting.result @@ -235,6 +265,9 @@ def test_native_wer_crash(cmake, httpserver, crash_arg, exception_code): assert len(httpserver.log) >= 1 envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) assert_native_crash(envelope, exception_code=exception_code) + event = envelope.get_event() + assert event["tags"]["test.initial-tag"] == "initial-value" + assert event["contexts"]["initial"]["foo"] == "bar" @pytest.mark.skipif(not has_oom, reason="OOM test unreliable in this environment") @@ -1765,3 +1798,36 @@ def test_native_acquired_scope(cmake, httpserver, clear): filenames = {item.headers.get("filename") for item in envelope} assert "CMakeCache.txt" in filenames assert ("scope.txt" in filenames) == (not clear) + + +def test_native_scope_snapshot(cmake, httpserver): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "native"}) + cmd = run_command(str(tmp_path / "sentry_test_integration")) + child = subprocess.Popen( + [*cmd, "initial-scope", "no-setup", "sleep"], + cwd=tmp_path, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + db_dir = tmp_path / ".sentry-native" + snapshot = None + + def scope_was_written(): + nonlocal snapshot + paths = list(db_dir.glob("*.run/__sentry-event")) + if not paths: + return False + try: + snapshot = json.loads(paths[0].read_text()) + except (OSError, json.JSONDecodeError): + return False + return snapshot.get("tags", {}).get("test.initial-tag") == "initial-value" + + try: + assert wait_for(scope_was_written) + assert snapshot["event_id"] + assert snapshot["level"] == "fatal" + assert "exception" not in snapshot + finally: + child.terminate() + child.wait() + assert wait_for(lambda: not list(db_dir.glob("*.run.daemon.lock"))) diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index b7685846bf..590a84b7f0 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -1,3 +1,4 @@ +import json import os import shutil import subprocess @@ -21,6 +22,7 @@ assert_breakpad_crash, assert_exception, wait_for, + assert_crash_stack, ) from .conditions import has_breakpad, has_files, is_qemu, is_wine @@ -182,11 +184,16 @@ def assert_crash_hint_attachments(envelope, callback): item.headers.get("filename") in ("CMakeCache.txt", "bytes.bin") for item in envelope ) - assert any( - item.headers.get("filename") == "callback.txt" - and item.payload.bytes == callback.replace("-", "_").encode() - for item in envelope + filename = callback.replace("-", "_") + ".json" + snapshot = json.loads( + next( + item.payload.bytes + for item in envelope + if item.headers.get("filename") == filename + ) ) + assert snapshot["level"] == "fatal" + return snapshot def test_inproc_crash_stdout(cmake): @@ -247,9 +254,10 @@ def test_inproc_crash_stdout_before_send(cmake): assert_no_crash_timestamp(has_files, tmp_path) assert_meta(envelope, integration="inproc") assert_breadcrumb(envelope) - assert_crash_hint_attachments(envelope, "before-send") + snapshot = assert_crash_hint_attachments(envelope, "before-send") assert_inproc_crash(envelope) assert_before_send(envelope) + assert_crash_stack(snapshot) @pytest.mark.skipif(is_qemu, reason="unreliable under qemu-user") @@ -276,8 +284,9 @@ def test_inproc_crash_stdout_before_send_and_on_crash(cmake): assert_no_crash_timestamp(has_files, tmp_path) assert_meta(envelope, integration="inproc") assert_breadcrumb(envelope) - assert_crash_hint_attachments(envelope, "on-crash") + snapshot = assert_crash_hint_attachments(envelope, "on-crash") assert_inproc_crash(envelope) + assert_crash_stack(snapshot) @pytest.mark.parametrize( diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index bb5329fc9b..719a7c1c07 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -77,6 +77,9 @@ target_link_libraries(sentry_test_unit PRIVATE if(MINGW) if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set_source_files_properties( + "${PROJECT_SOURCE_DIR}/src/unwinder/sentry_unwinder_dbghelp.c" + PROPERTIES COMPILE_OPTIONS -fms-extensions) target_link_options(sentry_test_unit PRIVATE -Wl,-pdb=) # IMPORTANT ! LLD must generate pdb files target_compile_options(sentry_test_unit PRIVATE -gcodeview) # IMPORTANT ! Clang must generate pdb to CodeView format else() From bcbca11551e88dc98a6dee6b3a014b941baa7192 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 15 Oct 2026 17:44:52 +0200 Subject: [PATCH 13/23] fix: provide `debug_meta.images` to `on_crash` (#2184) Promote the 5-6 years old module API, also used by downstream SDKs, now that it's usable for direct Native SDK users for comparing frame addresses with module ranges. Ref: #2102 Ref: #2116 --- CHANGELOG.md | 4 ++++ include/sentry.h | 4 ++-- src/backends/sentry_backend_inproc.c | 2 ++ src/backends/sentry_backend_native.c | 1 + src/sentry_core.c | 27 +++++++++++++++++++++++++-- src/sentry_core.h | 5 +++++ src/sentry_scope.c | 9 --------- src/sentry_scope.h | 2 +- tests/assertions.py | 20 ++++++++++++++++++++ tests/test_integration_native.py | 15 +++++---------- tests/test_integration_stdout.py | 23 +++++------------------ tests/unit/test_basic.c | 9 +++++++++ 12 files changed, 79 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3954b3afd0..dfa244533c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ - Add `sentry_scope_acquire` and `sentry_scope_release` to access the isolation scope. ([#2160](https://github.com/getsentry/sentry-native/pull/2160)) - Native: Provide exception metadata, registers, and a basic crashing-thread stack to `on_crash` for crash identification. ([#2174](https://github.com/getsentry/sentry-native/pull/2174)) +**Fixes**: + +- Native/Inproc: provide `debug_meta.images` in events passed to `on_crash`. ([#2184](https://github.com/getsentry/sentry-native/pull/2184)) + ## Unreleased **Features**: diff --git a/include/sentry.h b/include/sentry.h index 0909ac42d4..9638bab7b6 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2556,7 +2556,7 @@ SENTRY_API int sentry_shutdown(void); * Returns a new reference to an immutable, frozen list. * The reference must be released with `sentry_value_decref`. */ -SENTRY_EXPERIMENTAL_API sentry_value_t sentry_get_modules_list(void); +SENTRY_API sentry_value_t sentry_get_modules_list(void); /** * Clears the internal module cache. @@ -2567,7 +2567,7 @@ SENTRY_EXPERIMENTAL_API sentry_value_t sentry_get_modules_list(void); * `sentry_clear_modulecache` when doing so to make sure that the next call to * `sentry_capture_event` will have an up-to-date module list. */ -SENTRY_EXPERIMENTAL_API void sentry_clear_modulecache(void); +SENTRY_API void sentry_clear_modulecache(void); /** * Re-initializes the Sentry backend. diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index dbc454ef2c..fd5949df33 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -704,6 +704,8 @@ make_signal_event(const struct signal_slot *sig_slot, sentry_value_set_by_key(exc, "stacktrace", stacktrace); sentry_event_add_exception(event, exc); + sentry__event_ensure_modules(event); + return event; } diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 4b3c01c0ff..b0fd4713bc 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -1389,6 +1389,7 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) ctx, sentry__build_registers(uctx))); sentry_event_add_exception(event, exc); } + sentry__event_ensure_modules(event); sentry_hint_t hint; sentry__hint_init(&hint); diff --git a/src/sentry_core.c b/src/sentry_core.c index 7e9338809b..33188bbaac 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -851,6 +851,28 @@ prepare_attachments(sentry_hint_t *hint, sentry_scope_t *local_scope) return attachments; } +void +sentry__event_ensure_modules(sentry_value_t event) +{ + sentry_value_t debug_meta = sentry_value_get_by_key(event, "debug_meta"); + sentry_value_t images = sentry_value_get_by_key(debug_meta, "images"); + if (!sentry_value_is_null(images)) { + return; + } + + sentry_value_t modules = sentry_get_modules_list(); + if (!sentry_value_is_null(modules)) { + bool had_debug_meta = !sentry_value_is_null(debug_meta); + if (!had_debug_meta) { + debug_meta = sentry_value_new_object(); + } + sentry_value_set_by_key(debug_meta, "images", modules); + if (!had_debug_meta) { + sentry_value_set_by_key(event, "debug_meta", debug_meta); + } + } +} + sentry_value_t sentry__invoke_on_crash(const sentry_options_t *options, const sentry_ucontext_t *uctx, sentry_value_t event, sentry_hint_t *hint) @@ -904,6 +926,8 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event, sentry__record_errors_on_current_session(1); } + sentry__event_ensure_modules(event); + sentry_scope_mode_t mode = SENTRY_SCOPE_ALL; if (!options->symbolize_stacktraces) { mode &= ~SENTRY_SCOPE_STACKTRACES; @@ -956,8 +980,7 @@ sentry__prepare_transaction(const sentry_options_t *options, SENTRY_WITH_SCOPE (scope) { SENTRY_DEBUG("merging scope into transaction"); // Don't include debugging info - sentry_scope_mode_t mode = SENTRY_SCOPE_ALL & ~SENTRY_SCOPE_MODULES - & ~SENTRY_SCOPE_STACKTRACES; + sentry_scope_mode_t mode = SENTRY_SCOPE_ALL & ~SENTRY_SCOPE_STACKTRACES; sentry__scope_apply_to_event(scope, options, transaction, mode); } diff --git a/src/sentry_core.h b/src/sentry_core.h index 5b3141c09f..c2b6eda2c2 100644 --- a/src/sentry_core.h +++ b/src/sentry_core.h @@ -47,6 +47,11 @@ bool sentry__should_skip_upload(void); */ bool sentry__event_is_transaction(sentry_value_t event); +/** + * Populates missing `debug_meta.images` with cached modules when available. + */ +void sentry__event_ensure_modules(sentry_value_t event); + /** * Invokes the configured `on_crash` callback, if any. * diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 41b49b8612..6d508fb620 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -1551,15 +1551,6 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, } #if !defined(SENTRY_PLATFORM_NX) - if (mode & SENTRY_SCOPE_MODULES) { - sentry_value_t modules = sentry_get_modules_list(); - if (!sentry_value_is_null(modules)) { - sentry_value_t debug_meta = sentry_value_new_object(); - sentry_value_set_by_key(debug_meta, "images", modules); - sentry_value_set_by_key(event, "debug_meta", debug_meta); - } - } - if (mode & SENTRY_SCOPE_STACKTRACES) { sentry__foreach_stacktrace(event, sentry__symbolize_stacktrace); } diff --git a/src/sentry_scope.h b/src/sentry_scope.h index 2baabc8f50..f56e9f3e07 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -83,7 +83,7 @@ typedef enum { SENTRY_SCOPE_NONE = 0x0, // Add all the breadcrumbs from the scope to the event. SENTRY_SCOPE_BREADCRUMBS = 0x1, - // Add the module list to the event. + // TODO: remove SENTRY_SCOPE_MODULES = 0x2, // Symbolize all the stacktraces on-device which are found in the event. SENTRY_SCOPE_STACKTRACES = 0x4, diff --git a/tests/assertions.py b/tests/assertions.py index ed8c196196..183fa638b4 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -194,6 +194,8 @@ def assert_event_meta( "sentry_test_integration" in image["code_file"] for image in event["debug_meta"]["images"] ) + elif event.get("type") == "transaction": + assert "debug_meta" not in event def assert_debug_meta_images_do_not_overlap(event): @@ -455,6 +457,7 @@ def assert_exception(envelope): def assert_inproc_crash(envelope): event = envelope.get_event() assert_matches(event, {"level": "fatal"}) + assert event["debug_meta"]["images"] # depending on the unwinder, we currently don’t get any stack frames from # a `ucontext` assert_stacktrace(envelope, inside_exception=True, check_size=False) @@ -498,6 +501,23 @@ def assert_no_before_send(envelope): assert ("adapted_by", "before_send") not in event.items() +def assert_crash_hint_attachments(envelope, callback): + assert not any( + item.headers.get("filename") in ("CMakeCache.txt", "bytes.bin") + for item in envelope + ) + filename = callback.replace("-", "_") + ".json" + snapshot = json.loads( + next( + item.payload.bytes + for item in envelope + if item.headers.get("filename") == filename + ) + ) + assert snapshot["level"] == "fatal" + return snapshot + + def assert_before_breadcrumb(envelope): event = envelope.get_event() breadcrumbs = event.get("breadcrumbs", []) diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 37851019dc..e0eca2a93a 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -32,6 +32,7 @@ assert_attachment, assert_breadcrumb, assert_crash_timestamp, + assert_crash_hint_attachments, assert_no_crash_timestamp, assert_debug_meta_images_do_not_overlap, assert_meta, @@ -113,16 +114,10 @@ def test_native_crash_hint_attachments(cmake, httpserver, callback, crash): assert len(httpserver.log) >= 1 envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) - assert not any( - item.headers.get("filename") == "CMakeCache.txt" for item in envelope - ) - filename = callback.replace("-", "_") + ".json" - snapshot = json.loads( - next( - item.payload.bytes - for item in envelope - if item.headers.get("filename") == filename - ) + snapshot = assert_crash_hint_attachments(envelope, callback) + assert any( + "sentry_test_integration" in image["code_file"] + for image in snapshot["debug_meta"]["images"] ) assert_crash_stack(snapshot) if crash == "crash-null": diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index 590a84b7f0..984a9c69d4 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -1,4 +1,3 @@ -import json import os import shutil import subprocess @@ -14,6 +13,7 @@ assert_breadcrumb, assert_stacktrace, assert_event, + assert_event_meta, assert_inproc_crash, assert_minidump, assert_before_send, @@ -21,6 +21,7 @@ assert_no_crash_timestamp, assert_breakpad_crash, assert_exception, + assert_crash_hint_attachments, wait_for, assert_crash_stack, ) @@ -179,23 +180,6 @@ def run_crash_stdout_for(backend, cmake, example_args): return run_stdout_for(backend, cmake, ["attachment", "crash"] + example_args) -def assert_crash_hint_attachments(envelope, callback): - assert not any( - item.headers.get("filename") in ("CMakeCache.txt", "bytes.bin") - for item in envelope - ) - filename = callback.replace("-", "_") + ".json" - snapshot = json.loads( - next( - item.payload.bytes - for item in envelope - if item.headers.get("filename") == filename - ) - ) - assert snapshot["level"] == "fatal" - return snapshot - - def test_inproc_crash_stdout(cmake): tmp_path, output = run_crash_stdout_for("inproc", cmake, []) @@ -255,6 +239,8 @@ def test_inproc_crash_stdout_before_send(cmake): assert_meta(envelope, integration="inproc") assert_breadcrumb(envelope) snapshot = assert_crash_hint_attachments(envelope, "before-send") + assert_event_meta(snapshot, integrations=["inproc"]) + assert snapshot["debug_meta"] == envelope.get_event()["debug_meta"] assert_inproc_crash(envelope) assert_before_send(envelope) assert_crash_stack(snapshot) @@ -285,6 +271,7 @@ def test_inproc_crash_stdout_before_send_and_on_crash(cmake): assert_meta(envelope, integration="inproc") assert_breadcrumb(envelope) snapshot = assert_crash_hint_attachments(envelope, "on-crash") + assert snapshot["debug_meta"] == envelope.get_event()["debug_meta"] assert_inproc_crash(envelope) assert_crash_stack(snapshot) diff --git a/tests/unit/test_basic.c b/tests/unit/test_basic.c index caf2055869..e20c58de45 100644 --- a/tests/unit/test_basic.c +++ b/tests/unit/test_basic.c @@ -34,6 +34,11 @@ send_envelope_test_basic(sentry_envelope_t *envelope, void *data) const char *trans = sentry_value_as_string( sentry_value_get_by_key(event, "transaction")); TEST_CHECK_STRING_EQUAL(trans, "demo-trans"); + } else { + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(event, "debug_meta"), "custom")), + "kept"); } sentry_envelope_free(envelope); } @@ -63,6 +68,10 @@ SENTRY_TEST(basic_function_transport) sentry_value_t obj = sentry_value_new_object(); // something that is not a UUID, as this will be forcibly changed sentry_value_set_by_key(obj, "event_id", sentry_value_new_int32(1234)); + sentry_value_t debug_meta = sentry_value_new_object(); + sentry_value_set_by_key( + debug_meta, "custom", sentry_value_new_string("kept")); + sentry_value_set_by_key(obj, "debug_meta", debug_meta); sentry_capture_event(obj); sentry_user_consent_revoke(); From e550d2f1ed8b824817f153a1a4b1a13a9110f509 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 16 Oct 2026 07:35:17 +0200 Subject: [PATCH 14/23] fix(native): honor symbolication option for captured Windows frames Skip Windows frame symbol lookup when symbolize_stacktraces is disabled, including crash frames captured before daemon processing. --- src/backends/native/sentry_crash_daemon.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 21773313d8..6029e38016 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -694,8 +694,7 @@ static void enrich_frame_with_symbol( const sentry_crash_context_t *ctx, sentry_value_t frame, uint64_t addr) { - (void)ctx; - if (!g_stack_walk_process) { + if (!ctx->symbolize_stacktraces || !g_stack_walk_process) { return; } const size_t size = sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(WCHAR); From 0f5e3a2af1d727e8a882c7b1cad03d6568e280cf Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 16 Oct 2026 12:27:50 +0200 Subject: [PATCH 15/23] perf(native): Add IPC foundations (#2155) Add length-prefixed frames and app-to-daemon IPC based on Unix domain stream socket pairs and an anonymous pipe on Windows. Ref: #1834 --- src/backends/native/sentry_crash_daemon.c | 43 ++-- src/backends/native/sentry_crash_daemon.h | 8 +- src/backends/native/sentry_crash_ipc.c | 236 +++++++++++++++++++++- src/backends/native/sentry_crash_ipc.h | 75 ++++++- src/backends/sentry_backend_native.c | 63 ++++-- tests/unit/test_native_backend.c | 170 ++++++++++++++++ tests/unit/tests.inc | 6 + 7 files changed, 549 insertions(+), 52 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 6029e38016..ce9437baea 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -4460,29 +4460,29 @@ daemon_file_logger( #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) int -sentry__crash_daemon_main( - pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd) +sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, int notify_eventfd, + int ready_eventfd, int message_fd) #elif defined(SENTRY_PLATFORM_MACOS) int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, int notify_pipe_read, - int ready_pipe_write, int shm_fd) + int ready_pipe_write, int shm_fd, int message_fd) #elif defined(SENTRY_PLATFORM_WINDOWS) int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, - HANDLE ready_event_handle) + HANDLE ready_event_handle, HANDLE message_read_handle) #endif { // 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) sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon( - app_pid, app_tid, notify_eventfd, ready_eventfd); + app_pid, app_tid, notify_eventfd, ready_eventfd, message_fd); #elif defined(SENTRY_PLATFORM_MACOS) - sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon( - app_pid, app_tid, notify_pipe_read, ready_pipe_write, shm_fd); + sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon(app_pid, app_tid, + notify_pipe_read, ready_pipe_write, shm_fd, message_fd); #elif defined(SENTRY_PLATFORM_WINDOWS) - sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon( - app_pid, app_tid, event_handle, ready_event_handle); + sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_daemon(app_pid, app_tid, + event_handle, ready_event_handle, message_read_handle); #endif if (!ipc) { return 1; @@ -4647,6 +4647,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, // Don't overwrite it with the parent's handle (handles are per-process) (void)event_handle; (void)ready_event_handle; + (void)message_read_handle; #endif #if defined(SENTRY_PLATFORM_WINDOWS) @@ -4823,19 +4824,21 @@ main(int argc, char **argv) { // Expected arguments: // Linux: + // // macOS: + // # if defined(SENTRY_PLATFORM_MACOS) - if (argc < 6) { + if (argc < 7) { fprintf(stderr, "Usage: sentry-crash " - " \n"); + " \n"); return 1; } # else - if (argc < 5) { + if (argc < 6) { fprintf(stderr, "Usage: sentry-crash " - "\n"); + " \n"); return 1; } # endif @@ -4847,19 +4850,23 @@ main(int argc, char **argv) # if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) int notify_eventfd = atoi(argv[3]); int ready_eventfd = atoi(argv[4]); + int message_fd = atoi(argv[5]); return sentry__crash_daemon_main( - app_pid, app_tid, notify_eventfd, ready_eventfd); + app_pid, app_tid, notify_eventfd, ready_eventfd, message_fd); # elif defined(SENTRY_PLATFORM_MACOS) int notify_pipe_read = atoi(argv[3]); int ready_pipe_write = atoi(argv[4]); int shm_fd_arg = atoi(argv[5]); - return sentry__crash_daemon_main( - app_pid, app_tid, notify_pipe_read, ready_pipe_write, shm_fd_arg); + int message_fd = atoi(argv[6]); + return sentry__crash_daemon_main(app_pid, app_tid, notify_pipe_read, + ready_pipe_write, shm_fd_arg, message_fd); # elif defined(SENTRY_PLATFORM_WINDOWS) unsigned long long event_handle_val = strtoull(argv[3], NULL, 10); unsigned long long ready_event_val = strtoull(argv[4], NULL, 10); + unsigned long long message_handle_val = strtoull(argv[5], NULL, 10); HANDLE event_handle = (HANDLE)(uintptr_t)event_handle_val; HANDLE ready_event_handle = (HANDLE)(uintptr_t)ready_event_val; + HANDLE message_read_handle = (HANDLE)(uintptr_t)message_handle_val; # if defined(SENTRY_PLATFORM_XBOX) // Required before any XNetworking call the transport makes at @@ -4872,8 +4879,8 @@ main(int argc, char **argv) NULL, 0, sentry__xbox_network_prewarm_thread_proc, NULL, 0, NULL); # endif - int rv = sentry__crash_daemon_main( - app_pid, app_tid, event_handle, ready_event_handle); + int rv = sentry__crash_daemon_main(app_pid, app_tid, event_handle, + ready_event_handle, message_read_handle); # if defined(SENTRY_PLATFORM_XBOX) if (network_prewarm_thread) { diff --git a/src/backends/native/sentry_crash_daemon.h b/src/backends/native/sentry_crash_daemon.h index e689b39f8f..e7a6473d5d 100644 --- a/src/backends/native/sentry_crash_daemon.h +++ b/src/backends/native/sentry_crash_daemon.h @@ -22,14 +22,14 @@ struct sentry_options_s; * @param ready_handle Ready signal handle to signal parent */ #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) -int sentry__crash_daemon_main( - pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd); +int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, + int notify_eventfd, int ready_eventfd, int message_fd); #elif defined(SENTRY_PLATFORM_MACOS) int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, - int notify_pipe_read, int ready_pipe_write, int shm_fd); + int notify_pipe_read, int ready_pipe_write, int shm_fd, int message_fd); #elif defined(SENTRY_PLATFORM_WINDOWS) int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, - HANDLE event_handle, HANDLE ready_event_handle); + HANDLE event_handle, HANDLE ready_event_handle, HANDLE message_read_handle); #endif /** diff --git a/src/backends/native/sentry_crash_ipc.c b/src/backends/native/sentry_crash_ipc.c index deafe3ca51..94cdd0eef4 100644 --- a/src/backends/native/sentry_crash_ipc.c +++ b/src/backends/native/sentry_crash_ipc.c @@ -7,12 +7,151 @@ #include #include +static void +write_u16_le(char *buf, uint16_t value) +{ + buf[0] = (char)(value & 0xffu); + buf[1] = (char)((value >> 8) & 0xffu); +} + +static void +write_u32_le(char *buf, uint32_t value) +{ + buf[0] = (char)(value & 0xffu); + buf[1] = (char)((value >> 8) & 0xffu); + buf[2] = (char)((value >> 16) & 0xffu); + buf[3] = (char)((value >> 24) & 0xffu); +} + +static void +write_u64_le(char *buf, uint64_t value) +{ + for (size_t i = 0; i < 8; i++) { + buf[i] = (char)((value >> (i * 8)) & 0xffu); + } +} + +static uint16_t +read_u16_le(const char *buf) +{ + return (uint16_t)((uint8_t)buf[0] | ((uint16_t)(uint8_t)buf[1] << 8)); +} + +static uint32_t +read_u32_le(const char *buf) +{ + return (uint32_t)(uint8_t)buf[0] | ((uint32_t)(uint8_t)buf[1] << 8) + | ((uint32_t)(uint8_t)buf[2] << 16) | ((uint32_t)(uint8_t)buf[3] << 24); +} + +static uint64_t +read_u64_le(const char *buf) +{ + uint64_t value = 0; + for (size_t i = 0; i < 8; i++) { + value |= (uint64_t)(uint8_t)buf[i] << (i * 8); + } + return value; +} + +bool +sentry__crash_ipc_message_type_is_known(uint16_t type) +{ + return type >= SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT + && type <= SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN; +} + +sentry_crash_ipc_message_result_t +sentry__crash_ipc_message_encode(uint16_t type, uint16_t flags, + uint64_t sequence, const char *payload, size_t payload_len, char **out_buf, + size_t *out_len) +{ + if (!out_buf || !out_len || (payload_len && !payload) + || !sentry__crash_ipc_message_type_is_known(type)) { + return !sentry__crash_ipc_message_type_is_known(type) + ? SENTRY_CRASH_IPC_MESSAGE_UNKNOWN_TYPE + : SENTRY_CRASH_IPC_MESSAGE_INVALID; + } + + if (payload_len > SENTRY_CRASH_IPC_MESSAGE_MAX_LEN + - SENTRY_CRASH_IPC_MESSAGE_MIN_LEN + || payload_len > SIZE_MAX - SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE) { + return SENTRY_CRASH_IPC_MESSAGE_OVERSIZED; + } + + uint32_t message_len + = (uint32_t)(SENTRY_CRASH_IPC_MESSAGE_MIN_LEN + payload_len); + size_t total_len + = SENTRY_CRASH_IPC_MESSAGE_PREFIX_SIZE + (size_t)message_len; + char *buf = sentry_malloc(total_len); + if (!buf) { + return SENTRY_CRASH_IPC_MESSAGE_OOM; + } + + write_u32_le(buf, message_len); + write_u16_le(buf + 4, type); + write_u16_le(buf + 6, flags); + write_u64_le(buf + 8, sequence); + if (payload_len) { + memcpy( + buf + SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE, payload, payload_len); + } + + *out_buf = buf; + *out_len = total_len; + return SENTRY_CRASH_IPC_MESSAGE_OK; +} + +sentry_crash_ipc_message_result_t +sentry__crash_ipc_message_decode( + const char *buf, size_t buf_len, sentry_crash_ipc_message_t *message) +{ + if (!buf || !message) { + return SENTRY_CRASH_IPC_MESSAGE_INVALID; + } + if (buf_len < SENTRY_CRASH_IPC_MESSAGE_PREFIX_SIZE) { + return SENTRY_CRASH_IPC_MESSAGE_PARTIAL; + } + + uint32_t message_len = read_u32_le(buf); + if (message_len < SENTRY_CRASH_IPC_MESSAGE_MIN_LEN) { + return SENTRY_CRASH_IPC_MESSAGE_INVALID; + } +#if SIZE_MAX <= UINT32_MAX + if (message_len > SIZE_MAX - SENTRY_CRASH_IPC_MESSAGE_PREFIX_SIZE) { + return SENTRY_CRASH_IPC_MESSAGE_OVERSIZED; + } +#endif + + size_t total_len + = SENTRY_CRASH_IPC_MESSAGE_PREFIX_SIZE + (size_t)message_len; + if (buf_len < total_len) { + return SENTRY_CRASH_IPC_MESSAGE_PARTIAL; + } + if (buf_len > total_len) { + return SENTRY_CRASH_IPC_MESSAGE_INVALID; + } + + uint16_t type = read_u16_le(buf + 4); + if (!sentry__crash_ipc_message_type_is_known(type)) { + return SENTRY_CRASH_IPC_MESSAGE_UNKNOWN_TYPE; + } + + message->type = type; + message->flags = read_u16_le(buf + 6); + message->sequence = read_u64_le(buf + 8); + message->payload = buf + SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE; + message->payload_len = message_len - SENTRY_CRASH_IPC_MESSAGE_MIN_LEN; + return SENTRY_CRASH_IPC_MESSAGE_OK; +} + #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) # include # include # include # include +# include # include # include @@ -25,6 +164,8 @@ sentry__crash_ipc_init_app(sem_t *init_sem) } ipc->is_daemon = false; ipc->init_sem = init_sem; // Use provided semaphore (managed by backend) + ipc->message_fd[0] = -1; + ipc->message_fd[1] = -1; // Create shared memory with unique name based on PID and thread ID // macOS has a 31-character limit for POSIX shared memory names (PSEMNAMLEN) @@ -150,6 +291,24 @@ sentry__crash_ipc_init_app(sem_t *init_sem) return NULL; } + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, ipc->message_fd) + < 0) { + SENTRY_WARNF( + "failed to create message socketpair: %s", strerror(errno)); + close(ipc->ready_fd); + close(ipc->notify_fd); + munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); + close(ipc->shm_fd); + if (!shm_exists) { + shm_unlink(ipc->shm_name); + } + if (ipc->init_sem) { + sem_post(ipc->init_sem); + } + sentry_free(ipc); + return NULL; + } + // Initialize shared memory only if newly created if (!shm_exists) { sentry__crash_context_init(ipc->shmem); @@ -167,14 +326,16 @@ sentry__crash_ipc_init_app(sem_t *init_sem) } sentry_crash_ipc_t * -sentry__crash_ipc_init_daemon( - pid_t app_pid, uint64_t app_tid, int notify_eventfd, int ready_eventfd) +sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, + int notify_eventfd, int ready_eventfd, int message_fd) { sentry_crash_ipc_t *ipc = SENTRY_MAKE(sentry_crash_ipc_t); if (!ipc) { return NULL; } ipc->is_daemon = true; + ipc->message_fd[0] = -1; + ipc->message_fd[1] = message_fd; // Open existing shared memory created by app (using PID and thread ID) // Must match the format in sentry__crash_ipc_init_app @@ -299,6 +460,13 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) close(ipc->ready_fd); } + if (ipc->message_fd[0] >= 0) { + close(ipc->message_fd[0]); + } + if (ipc->message_fd[1] >= 0) { + close(ipc->message_fd[1]); + } + sentry_free(ipc); } @@ -309,6 +477,7 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) # include # include # include +# include # include # include @@ -321,6 +490,8 @@ sentry__crash_ipc_init_app(sentry_mutex_t *init_mutex) } ipc->is_daemon = false; ipc->init_mutex = init_mutex; + ipc->message_fd[0] = -1; + ipc->message_fd[1] = -1; // Build a file path for shared memory using the system temp directory. // Unlike shm_open(), regular files in $TMPDIR work inside App Sandbox. @@ -445,6 +616,27 @@ sentry__crash_ipc_init_app(sentry_mutex_t *init_mutex) return NULL; } + if (socketpair(AF_UNIX, SOCK_STREAM, 0, ipc->message_fd) < 0) { + SENTRY_WARNF( + "failed to create message socketpair: %s", strerror(errno)); + close(ipc->ready_pipe[0]); + close(ipc->ready_pipe[1]); + close(ipc->notify_pipe[0]); + close(ipc->notify_pipe[1]); + munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); + close(ipc->shm_fd); + if (!shm_exists) { + unlink(ipc->shm_path); + } + if (ipc->init_mutex) { + sentry__mutex_unlock(ipc->init_mutex); + } + sentry_free(ipc); + return NULL; + } + fcntl(ipc->message_fd[0], F_SETFD, FD_CLOEXEC); + fcntl(ipc->message_fd[1], F_SETFD, FD_CLOEXEC); + if (!shm_exists) { sentry__crash_context_init(ipc->shmem); } @@ -461,7 +653,7 @@ sentry__crash_ipc_init_app(sentry_mutex_t *init_mutex) sentry_crash_ipc_t * sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, - int notify_pipe_read, int ready_pipe_write, int shm_fd) + int notify_pipe_read, int ready_pipe_write, int shm_fd, int message_fd) { (void)app_pid; (void)app_tid; @@ -471,6 +663,8 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, return NULL; } ipc->is_daemon = true; + ipc->message_fd[0] = -1; + ipc->message_fd[1] = message_fd; // Use the inherited shm_fd directly (no shm_open needed, sandbox-safe) ipc->shm_fd = shm_fd; @@ -584,6 +778,13 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) close(ipc->ready_pipe[1]); } + if (ipc->message_fd[0] >= 0) { + close(ipc->message_fd[0]); + } + if (ipc->message_fd[1] >= 0) { + close(ipc->message_fd[1]); + } + if (!ipc->is_daemon) { sentry__crash_ipc_unlink(ipc); } @@ -697,6 +898,25 @@ sentry__crash_ipc_init_app(HANDLE init_mutex) return NULL; } + SECURITY_ATTRIBUTES pipe_attrs; + ZeroMemory(&pipe_attrs, sizeof(pipe_attrs)); + pipe_attrs.nLength = sizeof(pipe_attrs); + pipe_attrs.bInheritHandle = TRUE; + if (!CreatePipe(&ipc->message_read_handle, &ipc->message_write_handle, + &pipe_attrs, 0)) { + SENTRY_WARNF("failed to create message pipe: %lu", GetLastError()); + CloseHandle(ipc->ready_event_handle); + CloseHandle(ipc->event_handle); + UnmapViewOfFile(ipc->shmem); + CloseHandle(ipc->shm_handle); + if (ipc->init_mutex) { + ReleaseMutex(ipc->init_mutex); + } + sentry_free(ipc); + return NULL; + } + SetHandleInformation(ipc->message_write_handle, HANDLE_FLAG_INHERIT, 0); + // Initialize shared memory only if newly created if (!shm_exists) { sentry__crash_context_init(ipc->shmem); @@ -714,7 +934,7 @@ sentry__crash_ipc_init_app(HANDLE init_mutex) sentry_crash_ipc_t * sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, - HANDLE event_handle, HANDLE ready_event_handle) + HANDLE event_handle, HANDLE ready_event_handle, HANDLE message_read_handle) { // On Windows, we open events by name, so handles from parent are not used // (handles are per-process and cannot be directly inherited) @@ -726,6 +946,7 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, return NULL; } ipc->is_daemon = true; + ipc->message_read_handle = message_read_handle; // Open existing shared memory (using PID and thread ID) swprintf(ipc->shm_name, SENTRY_CRASH_IPC_NAME_SIZE, @@ -855,6 +1076,13 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) CloseHandle(ipc->ready_event_handle); } + if (ipc->message_read_handle) { + CloseHandle(ipc->message_read_handle); + } + if (ipc->message_write_handle) { + CloseHandle(ipc->message_write_handle); + } + if (ipc->parent_handle) { CloseHandle(ipc->parent_handle); } diff --git a/src/backends/native/sentry_crash_ipc.h b/src/backends/native/sentry_crash_ipc.h index ad0e4fa6fe..dbefd8b3b8 100644 --- a/src/backends/native/sentry_crash_ipc.h +++ b/src/backends/native/sentry_crash_ipc.h @@ -4,6 +4,9 @@ #include "sentry_boot.h" #include "sentry_crash_context.h" +#include +#include + #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) # include # include @@ -16,6 +19,57 @@ # include #endif +#define SENTRY_CRASH_IPC_MESSAGE_PREFIX_SIZE 4u +#define SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE 16u +#define SENTRY_CRASH_IPC_MESSAGE_MIN_LEN 12u +#define SENTRY_CRASH_IPC_MESSAGE_MAX_LEN UINT32_MAX + +typedef enum { + SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT = 1, + SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE = 2, + SENTRY_CRASH_IPC_MESSAGE_SET_ENVIRONMENT = 3, + SENTRY_CRASH_IPC_MESSAGE_SET_TRANSACTION = 4, + SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT = 5, + SENTRY_CRASH_IPC_MESSAGE_SET_LEVEL = 6, + SENTRY_CRASH_IPC_MESSAGE_SET_USER = 7, + SENTRY_CRASH_IPC_MESSAGE_SET_TAG = 8, + SENTRY_CRASH_IPC_MESSAGE_REMOVE_TAG = 9, + SENTRY_CRASH_IPC_MESSAGE_SET_EXTRA = 10, + SENTRY_CRASH_IPC_MESSAGE_REMOVE_EXTRA = 11, + SENTRY_CRASH_IPC_MESSAGE_SET_CONTEXT = 12, + SENTRY_CRASH_IPC_MESSAGE_REMOVE_CONTEXT = 13, + SENTRY_CRASH_IPC_MESSAGE_ADD_BREADCRUMB = 14, + SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST = 15, + SENTRY_CRASH_IPC_MESSAGE_CRASH = 16, + SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN = 17, +} sentry_crash_ipc_message_type_t; + +typedef enum { + SENTRY_CRASH_IPC_MESSAGE_OK = 0, + SENTRY_CRASH_IPC_MESSAGE_PARTIAL = 1, + SENTRY_CRASH_IPC_MESSAGE_INVALID = 2, + SENTRY_CRASH_IPC_MESSAGE_OVERSIZED = 3, + SENTRY_CRASH_IPC_MESSAGE_UNKNOWN_TYPE = 4, + SENTRY_CRASH_IPC_MESSAGE_OOM = 5, +} sentry_crash_ipc_message_result_t; + +typedef struct { + uint16_t type; + uint16_t flags; + uint64_t sequence; + const char *payload; + size_t payload_len; +} sentry_crash_ipc_message_t; + +bool sentry__crash_ipc_message_type_is_known(uint16_t type); + +sentry_crash_ipc_message_result_t sentry__crash_ipc_message_encode( + uint16_t type, uint16_t flags, uint64_t sequence, const char *payload, + size_t payload_len, char **out_buf, size_t *out_len); + +sentry_crash_ipc_message_result_t sentry__crash_ipc_message_decode( + const char *buf, size_t buf_len, sentry_crash_ipc_message_t *message); + #if defined(SENTRY_PLATFORM_WINDOWS) typedef HANDLE sentry_process_handle_t; #else @@ -32,6 +86,8 @@ typedef struct { int shm_fd; int notify_fd; // Eventfd for crash notifications int ready_fd; // Eventfd for daemon ready signal + // Anonymous app-to-daemon message stream: app writes, daemon reads. + int message_fd[2]; char shm_name[SENTRY_CRASH_IPC_NAME_SIZE]; sem_t *init_sem; // Named semaphore for initialization synchronization char sem_name[SENTRY_CRASH_IPC_NAME_SIZE]; @@ -39,14 +95,19 @@ typedef struct { int shm_fd; int notify_pipe[2]; // Pipe for crash notifications (fork-safe) int ready_pipe[2]; // Pipe for daemon ready signal (fork-safe) + int message_fd[2]; // Anonymous app-to-daemon message stream: app writes, + // daemon reads char shm_path[SENTRY_CRASH_MAX_PATH]; // File-backed shm path (sandbox-safe) sentry_mutex_t *init_mutex; // Process-wide mutex (sandbox-safe, no sem_open) #elif defined(SENTRY_PLATFORM_WINDOWS) HANDLE shm_handle; HANDLE event_handle; // Event for crash notifications (parent -> daemon) - HANDLE - ready_event_handle; // Event for daemon ready signal (daemon -> parent) + HANDLE ready_event_handle; // Event for ready signal (daemon -> parent) + HANDLE message_read_handle; // Anonymous app-to-daemon message stream read + // end + HANDLE message_write_handle; // Anonymous app-to-daemon message stream write + // end wchar_t shm_name[SENTRY_CRASH_IPC_NAME_SIZE]; wchar_t event_name[SENTRY_CRASH_IPC_NAME_SIZE]; wchar_t ready_event_name[SENTRY_CRASH_IPC_NAME_SIZE]; @@ -85,14 +146,16 @@ sentry_crash_ipc_t *sentry__crash_ipc_init_app(void); * Linux, pipe fd on macOS, event on Windows) */ #if defined(SENTRY_PLATFORM_LINUX) || 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); +sentry_crash_ipc_t *sentry__crash_ipc_init_daemon(pid_t app_pid, + uint64_t app_tid, int notify_eventfd, int ready_eventfd, int message_fd); #elif defined(SENTRY_PLATFORM_MACOS) sentry_crash_ipc_t *sentry__crash_ipc_init_daemon(pid_t app_pid, - uint64_t app_tid, int notify_pipe_read, int ready_pipe_write, int shm_fd); + uint64_t app_tid, int notify_pipe_read, int ready_pipe_write, int shm_fd, + int message_fd); #elif defined(SENTRY_PLATFORM_WINDOWS) sentry_crash_ipc_t *sentry__crash_ipc_init_daemon(pid_t app_pid, - uint64_t app_tid, HANDLE event_handle, HANDLE ready_event_handle); + uint64_t app_tid, HANDLE event_handle, HANDLE ready_event_handle, + HANDLE message_read_handle); #endif /** diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 6bf029278c..64dc467dc6 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -333,15 +333,16 @@ native_backend_process_old_run(sentry_backend_t *backend, #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) static pid_t daemon_start(pid_t app_pid, uint64_t app_tid, int notify_eventfd, - int ready_eventfd, const char *handler_path) + int ready_eventfd, int message_fd, const char *handler_path) #elif defined(SENTRY_PLATFORM_MACOS) static pid_t daemon_start(pid_t app_pid, uint64_t app_tid, int notify_pipe_read, - int ready_pipe_write, int shm_fd, const char *handler_path) + int ready_pipe_write, int shm_fd, int message_fd, const char *handler_path) #elif defined(SENTRY_PLATFORM_WINDOWS) static pid_t daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, - HANDLE ready_event_handle, const char *handler_path) + HANDLE ready_event_handle, HANDLE message_read_handle, + const char *handler_path) #endif { #if defined(SENTRY_PLATFORM_MACOS) @@ -374,16 +375,19 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, strcpy(daemon_path + dir_len, "sentry-crash"); } - // Build argument strings (6 args: pid, tid, notify_fd, ready_fd, shm_fd) - char pid_str[32], tid_str[32], notify_str[32], ready_str[32], shm_str[32]; + // Build argument strings (pid, tid, notify_fd, ready_fd, shm_fd, + // message_fd) + char pid_str[32], tid_str[32], notify_str[32], ready_str[32], shm_str[32], + message_str[32]; snprintf(pid_str, sizeof(pid_str), "%d", (int)app_pid); snprintf(tid_str, sizeof(tid_str), "%" PRIx64, app_tid); snprintf(notify_str, sizeof(notify_str), "%d", notify_pipe_read); snprintf(ready_str, sizeof(ready_str), "%d", ready_pipe_write); snprintf(shm_str, sizeof(shm_str), "%d", shm_fd); + snprintf(message_str, sizeof(message_str), "%d", message_fd); char *spawn_argv[] = { "sentry-crash", pid_str, tid_str, notify_str, - ready_str, shm_str, NULL }; + ready_str, shm_str, message_str, NULL }; // Set up posix_spawn attributes posix_spawnattr_t attr; @@ -399,6 +403,7 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, posix_spawn_file_actions_addinherit_np(&file_actions, notify_pipe_read); posix_spawn_file_actions_addinherit_np(&file_actions, ready_pipe_write); posix_spawn_file_actions_addinherit_np(&file_actions, shm_fd); + posix_spawn_file_actions_addinherit_np(&file_actions, message_fd); // Open /dev/null on stdin/stdout/stderr so the daemon starts with valid // standard fds. Without this, POSIX_SPAWN_CLOEXEC_DEFAULT closes them, // and the first fopen() in the daemon would get fd 0, which the daemon's @@ -409,7 +414,7 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, int std_modes[3] = { O_RDONLY, O_WRONLY, O_WRONLY }; for (int i = 0; i < 3; i++) { if (std_fds[i] != notify_pipe_read && std_fds[i] != ready_pipe_write - && std_fds[i] != shm_fd) { + && std_fds[i] != shm_fd && std_fds[i] != message_fd) { posix_spawn_file_actions_addopen( &file_actions, std_fds[i], "/dev/null", std_modes[i], 0); } @@ -450,16 +455,22 @@ 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); } + int message_flags = fcntl(message_fd, F_GETFD); + if (message_flags != -1) { + fcntl(message_fd, F_SETFD, message_flags & ~FD_CLOEXEC); + } // Convert arguments to strings for exec - char pid_str[32], tid_str[32], notify_str[32], ready_str[32]; + char pid_str[32], tid_str[32], notify_str[32], ready_str[32], + message_str[32]; snprintf(pid_str, sizeof(pid_str), "%d", (int)app_pid); snprintf(tid_str, sizeof(tid_str), "%" PRIx64, app_tid); snprintf(notify_str, sizeof(notify_str), "%d", notify_eventfd); snprintf(ready_str, sizeof(ready_str), "%d", ready_eventfd); + snprintf(message_str, sizeof(message_str), "%d", message_fd); - char *argv[] - = { "sentry-crash", pid_str, tid_str, notify_str, ready_str, NULL }; + char *argv[] = { "sentry-crash", pid_str, tid_str, notify_str, + ready_str, message_str, NULL }; if (!sentry__string_empty(handler_path)) { execv(handler_path, argv); @@ -542,13 +553,14 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, } // Build command line: sentry-crash.exe - // - wchar_t cmd_line[SENTRY_CRASH_MAX_PATH + 128]; + // + wchar_t cmd_line[SENTRY_CRASH_MAX_PATH + 192]; int cmd_len = _snwprintf(cmd_line, sizeof(cmd_line) / sizeof(wchar_t), - L"\"%s\" %lu %llx %llu %llu", daemon_path_w, (unsigned long)app_pid, - (unsigned long long)app_tid, + L"\"%s\" %lu %llx %llu %llu %llu", daemon_path_w, + (unsigned long)app_pid, (unsigned long long)app_tid, (unsigned long long)(uintptr_t)event_handle, - (unsigned long long)(uintptr_t)ready_event_handle); + (unsigned long long)(uintptr_t)ready_event_handle, + (unsigned long long)(uintptr_t)message_read_handle); if (cmd_len < 0 || cmd_len >= (int)(sizeof(cmd_line) / sizeof(wchar_t))) { SENTRY_WARN("Command line too long for daemon spawn"); @@ -937,16 +949,17 @@ native_backend_startup( # if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) uint64_t tid = (uint64_t)pthread_self(); state->daemon_pid = daemon_start(getpid(), tid, state->ipc->notify_fd, - state->ipc->ready_fd, daemon_handler_path); + state->ipc->ready_fd, state->ipc->message_fd[1], daemon_handler_path); # elif defined(SENTRY_PLATFORM_MACOS) uint64_t tid = (uint64_t)pthread_self(); state->daemon_pid = daemon_start(getpid(), tid, state->ipc->notify_pipe[0], - state->ipc->ready_pipe[1], state->ipc->shm_fd, daemon_handler_path); + state->ipc->ready_pipe[1], state->ipc->shm_fd, + state->ipc->message_fd[1], daemon_handler_path); # elif defined(SENTRY_PLATFORM_WINDOWS) uint64_t tid = (uint64_t)GetCurrentThreadId(); - state->daemon_pid - = daemon_start(GetCurrentProcessId(), tid, state->ipc->event_handle, - state->ipc->ready_event_handle, daemon_handler_path); + state->daemon_pid = daemon_start(GetCurrentProcessId(), tid, + state->ipc->event_handle, state->ipc->ready_event_handle, + state->ipc->message_read_handle, daemon_handler_path); # endif // On Windows, pid_t is DWORD (unsigned), so (pid_t)-1 == 0xFFFFFFFF. @@ -970,11 +983,16 @@ native_backend_startup( // Close unused pipe ends in parent process close(state->ipc->notify_pipe[0]); // Daemon reads from this close(state->ipc->ready_pipe[1]); // Daemon writes to this + close(state->ipc->message_fd[1]); // Daemon reads from this state->ipc->notify_pipe[0] = -1; state->ipc->ready_pipe[1] = -1; + state->ipc->message_fd[1] = -1; # endif # if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + close(state->ipc->message_fd[1]); // Daemon reads from this + state->ipc->message_fd[1] = -1; + // Close unused eventfd ends in parent process // (eventfds are bidirectional, but we only use one direction per fd) // Parent writes to notify_fd, daemon reads from it - parent can close for @@ -994,6 +1012,11 @@ native_backend_startup( } # endif +# if defined(SENTRY_PLATFORM_WINDOWS) + CloseHandle(state->ipc->message_read_handle); + state->ipc->message_read_handle = NULL; +# endif + // Wait for daemon to signal it's ready if (!sentry__crash_ipc_wait_for_ready( state->ipc, SENTRY_CRASH_DAEMON_READY_TIMEOUT_MS)) { diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index 1395c5c90e..f91860946b 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -16,6 +16,7 @@ // Include native backend headers # include "../../src/backends/native/minidump/sentry_minidump_format.h" # include "../../src/backends/native/sentry_crash_context.h" +# include "../../src/backends/native/sentry_crash_ipc.h" static void noop_crashed_last_run(const sentry_envelope_t *envelope, void *user_data) @@ -813,3 +814,172 @@ SENTRY_TEST(elf_header_entry_sizes) TEST_CHECK(!sentry__elf_has_phdr_size(e_ident, phdr_size)); #endif } + +SENTRY_TEST(crash_ipc_message_roundtrip) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + const char payload[] = "\x81\xa3key\xa5value"; + char *buf = NULL; + size_t buf_len = 0; + + sentry_crash_ipc_message_result_t result + = sentry__crash_ipc_message_encode(SENTRY_CRASH_IPC_MESSAGE_SET_TAG, 7, + 42, payload, sizeof(payload) - 1, &buf, &buf_len); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_OK); + TEST_CHECK(buf != NULL); + TEST_CHECK_INT_EQUAL( + buf_len, SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE + sizeof(payload) - 1); + + sentry_crash_ipc_message_t message; + result = sentry__crash_ipc_message_decode(buf, buf_len, &message); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_OK); + TEST_CHECK_INT_EQUAL(message.type, SENTRY_CRASH_IPC_MESSAGE_SET_TAG); + TEST_CHECK_INT_EQUAL(message.flags, 7); + TEST_CHECK_INT_EQUAL(message.sequence, 42); + TEST_CHECK_INT_EQUAL(message.payload_len, sizeof(payload) - 1); + TEST_CHECK(memcmp(message.payload, payload, sizeof(payload) - 1) == 0); + + sentry_free(buf); +#endif +} + +SENTRY_TEST(crash_ipc_message_partial) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + char *buf = NULL; + size_t buf_len = 0; + + sentry_crash_ipc_message_result_t result = sentry__crash_ipc_message_encode( + SENTRY_CRASH_IPC_MESSAGE_CRASH, 0, 9, NULL, 0, &buf, &buf_len); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_OK); + + sentry_crash_ipc_message_t message; + result = sentry__crash_ipc_message_decode(buf, 3, &message); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_PARTIAL); + + result = sentry__crash_ipc_message_decode(buf, buf_len - 1, &message); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_PARTIAL); + + sentry_free(buf); +#endif +} + +SENTRY_TEST(crash_ipc_message_invalid_lengths) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + char too_small_len[4] = { 11, 0, 0, 0 }; + sentry_crash_ipc_message_t message; + + sentry_crash_ipc_message_result_t result = sentry__crash_ipc_message_decode( + too_small_len, sizeof(too_small_len), &message); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_INVALID); + + char *buf = NULL; + size_t buf_len = 0; + result = sentry__crash_ipc_message_encode( + SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN, 0, 1, NULL, 0, &buf, &buf_len); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_OK); + + char extra[32]; + memcpy(extra, buf, buf_len); + extra[buf_len] = 0; + result = sentry__crash_ipc_message_decode(extra, buf_len + 1, &message); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_INVALID); + + sentry_free(buf); +#endif +} + +SENTRY_TEST(crash_ipc_message_unknown_type) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + char *buf = NULL; + size_t buf_len = 0; + + sentry_crash_ipc_message_result_t result = sentry__crash_ipc_message_encode( + SENTRY_CRASH_IPC_MESSAGE_CRASH, 0, 1, NULL, 0, &buf, &buf_len); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_OK); + buf[4] = 0xff; + buf[5] = 0x7f; + + sentry_crash_ipc_message_t message; + result = sentry__crash_ipc_message_decode(buf, buf_len, &message); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_UNKNOWN_TYPE); + + sentry_free(buf); +#endif +} + +SENTRY_TEST(crash_ipc_message_oversized) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + char oversized_len[4] = { 0 }; + uint32_t len = SENTRY_CRASH_IPC_MESSAGE_MAX_LEN; + oversized_len[0] = (char)(len & 0xffu); + oversized_len[1] = (char)((len >> 8) & 0xffu); + oversized_len[2] = (char)((len >> 16) & 0xffu); + oversized_len[3] = (char)((len >> 24) & 0xffu); + + sentry_crash_ipc_message_t message; + sentry_crash_ipc_message_result_t result = sentry__crash_ipc_message_decode( + oversized_len, sizeof(oversized_len), &message); +# if SIZE_MAX <= UINT32_MAX + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_OVERSIZED); +# else + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_PARTIAL); +# endif + + char payload = 0; + char *buf = NULL; + size_t buf_len = 0; + result = sentry__crash_ipc_message_encode( + SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, 0, 0, &payload, + SENTRY_CRASH_IPC_MESSAGE_MAX_LEN, &buf, &buf_len); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_OVERSIZED); + result = sentry__crash_ipc_message_encode( + SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, 0, 0, &payload, + SIZE_MAX - SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE + 1, &buf, &buf_len); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_OVERSIZED); +#endif +} + +SENTRY_TEST(crash_ipc_message_large_roundtrip) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + size_t payload_len = 200u * 1024u * 1024u; + char *payload = sentry_malloc(payload_len); + TEST_ASSERT(payload != NULL); + memset(payload, 0xa5, payload_len); + char *buf = NULL; + size_t buf_len = 0; + + sentry_crash_ipc_message_result_t result = sentry__crash_ipc_message_encode( + SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, 0, 42, payload, payload_len, + &buf, &buf_len); + TEST_ASSERT(result == SENTRY_CRASH_IPC_MESSAGE_OK); + TEST_CHECK(buf_len == SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE + payload_len); + + sentry_crash_ipc_message_t message; + result = sentry__crash_ipc_message_decode(buf, buf_len - 1, &message); + TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_PARTIAL); + result = sentry__crash_ipc_message_decode(buf, buf_len, &message); + TEST_ASSERT(result == SENTRY_CRASH_IPC_MESSAGE_OK); + TEST_CHECK(message.payload_len == payload_len); + TEST_CHECK(memcmp(message.payload, payload, payload_len) == 0); + + sentry_free(buf); + sentry_free(payload); +#endif +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 157cba9299..4af1367126 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -142,6 +142,12 @@ XX(crash_context_init) XX(crash_context_null_options) XX(crash_context_options_propagation) XX(crash_context_transport_fields) +XX(crash_ipc_message_invalid_lengths) +XX(crash_ipc_message_large_roundtrip) +XX(crash_ipc_message_oversized) +XX(crash_ipc_message_partial) +XX(crash_ipc_message_roundtrip) +XX(crash_ipc_message_unknown_type) XX(crash_marker) XX(crashed_last_run) XX(custom_logger) From 4ce28e77b5a9216fa3ee456ea85ba3113d7c59d0 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 16 Oct 2026 12:27:51 +0200 Subject: [PATCH 16/23] perf(native): Sync scope over IPC (#2157) * perf(native): Sync scope over IPC Instead of flushing the whole scope/event on disk on every scope change, send an initial scope snapshot and then minimal incremental updates to the crash daemon over IPC. Keep the latest complete state in daemon memory for crash reports. Discard interrupted messages and preserve crash callback changes through a final event and attachment snapshot. Close: #1834 * handle SENTRY_CRASH_STATE_CRASHING * signal.h --- CHANGELOG.md | 1 + src/backends/native/sentry_crash_context.h | 4 +- src/backends/native/sentry_crash_daemon.c | 392 ++++++-------- src/backends/native/sentry_crash_ipc.c | 485 ++++++++++++++++- src/backends/native/sentry_crash_ipc.h | 54 +- src/backends/sentry_backend_native.c | 600 ++++++++++++--------- src/integrations/sentry_integration_wer.c | 5 +- src/sentry_scope.c | 97 +++- src/sentry_scope.h | 16 +- tests/test_integration_native.py | 223 ++++---- tests/unit/test_native_backend.c | 558 ++++++++++++++++++- tests/unit/test_scope.c | 166 +++++- tests/unit/tests.inc | 13 + 13 files changed, 2003 insertions(+), 611 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26067c939c..8cb9fdc9cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Add `sentry_scope_begin_read`/`sentry_scope_end_read` and `sentry_scope_begin_write`/`sentry_scope_end_write` for consistent reads of multiple scope properties and applying multiple scope changes with a single scope flush. ([#2107](https://github.com/getsentry/sentry-native/pull/2107)) - Add `sentry_scope_acquire` and `sentry_scope_release` to access the isolation scope. ([#2160](https://github.com/getsentry/sentry-native/pull/2160)) - Native: Provide exception metadata, registers, and a basic crashing-thread stack to `on_crash` for crash identification. ([#2174](https://github.com/getsentry/sentry-native/pull/2174)) +- Native: reduce disk I/O on scope changes by sending incremental updates to the crash daemon over IPC. ([#2155](https://github.com/getsentry/sentry-native/pull/2155), [#2157](https://github.com/getsentry/sentry-native/pull/2157)) **Fixes**: diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index 627c415d40..4f5e30e17c 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -330,13 +330,11 @@ typedef struct { // On Linux, libunwind captures using DWARF without frame pointers. // The daemon prefers captures over its own stack walk. sentry_crash_capture_t capture; + bool event_discarded; // Sentry-specific metadata paths char database_path[SENTRY_CRASH_MAX_PATH]; // Shared across runs char run_path[SENTRY_CRASH_MAX_PATH]; // For current run - char event_path[SENTRY_CRASH_MAX_PATH]; - char breadcrumb1_path[SENTRY_CRASH_MAX_PATH]; - char breadcrumb2_path[SENTRY_CRASH_MAX_PATH]; char envelope_path[SENTRY_CRASH_MAX_PATH]; char external_reporter_path[SENTRY_CRASH_MAX_PATH]; char dsn[SENTRY_CRASH_MAX_PATH]; // Sentry DSN for uploading crashes diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index ce9437baea..3d983af555 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -374,7 +374,7 @@ read_legacy_manifest(const sentry_path_t *manifest_path) size_t buf_len = 0; char *buf = sentry__path_read_to_buffer(manifest_path, &buf_len); if (!buf) { - return sentry_value_new_list(); + return sentry_value_new_null(); } const char *start = buf; @@ -444,42 +444,37 @@ read_attachment_manifest(const sentry_path_t *run_folder) return attachments; } -static void -write_attachments_from_manifest( - int fd, const sentry_options_t *options, const sentry_path_t *run_folder) +static sentry_value_t +read_crash_event(const sentry_path_t *run_folder) { - sentry_value_t attachments = read_attachment_manifest(run_folder); - size_t len = sentry_value_get_length(attachments); - for (size_t i = 0; i < len; i++) { - sentry_value_t attachment = sentry_value_get_by_index(attachments, i); - const char *path = sentry__attachment_get_path(attachment); - if (!attachment_is_placeholder(options, path)) { - write_attachment_to_envelope(fd, path, - sentry__attachment_get_filename(attachment), - sentry__attachment_get_type(attachment), - sentry__attachment_get_content_type(attachment)); - } + sentry_path_t *path = sentry__path_join_str(run_folder, "__sentry-event"); + if (!path) { + return sentry_value_new_null(); } - sentry_value_decref(attachments); + size_t len = 0; + char *buf = sentry__path_read_to_buffer(path, &len); + sentry__path_free(path); + if (!buf) { + return sentry_value_new_null(); + } + sentry_value_t event = sentry_value_from_json(buf, len); + sentry_free(buf); + if (sentry_value_get_type(event) != SENTRY_VALUE_TYPE_OBJECT) { + sentry_value_decref(event); + return sentry_value_new_null(); + } + return event; } -// For each large attachment listed in `/__sentry-attachments`, -// cache it as an attachment-ref item. Small attachments were already inlined -// during envelope writing. +// cache large staged attachments as attachment-ref items static void add_attachment_refs(sentry_envelope_t *envelope, - const sentry_options_t *options, const sentry_path_t *run_folder) + const sentry_options_t *options, sentry_value_t list) { if (!envelope || !options || !options->run - || !options->enable_large_attachments || !run_folder) { - return; - } - sentry_value_t list = read_attachment_manifest(run_folder); - if (sentry_value_is_null(list)) { - SENTRY_WARN("Failed to parse attachment manifest"); + || !options->enable_large_attachments) { return; } - bool materialized = false; size_t len = sentry_value_get_length(list); for (size_t i = 0; i < len; i++) { @@ -497,7 +492,6 @@ add_attachment_refs(sentry_envelope_t *envelope, SENTRY_WARN("failed to cache attachment-ref"); } } - sentry_value_decref(list); } /** @@ -3087,64 +3081,6 @@ build_stacktrace_from_ctx(const sentry_crash_context_t *ctx) return build_stacktrace_for_thread(ctx, SIZE_MAX); } -/** - * Reads one breadcrumb ring file the crashing process appended on its hot path - * into a breadcrumb list. Returns null if the file is absent or empty. - */ -static sentry_value_t -read_breadcrumb_ring_file(const sentry_path_t *run_folder, const char *name) -{ - if (!run_folder) { - return sentry_value_new_null(); - } - sentry_path_t *path = sentry__path_join_str(run_folder, name); - if (!path) { - return sentry_value_new_null(); - } - size_t size = 0; - char *buf = sentry__path_read_to_buffer(path, &size); - sentry__path_free(path); - if (!buf || size == 0) { - sentry_free(buf); - return sentry_value_new_null(); - } - sentry_value_t list = sentry_value_from_msgpack_stream(buf, size); - sentry_free(buf); - return list; -} - -/** - * Assembles the crash event's breadcrumbs from the two ring files the crashing - * process appended one-at-a-time, merges them in timestamp order, keeps the - * newest `max_breadcrumbs`, and attaches them to `event`. - * Mirrors the crashpad backend's `report_to_envelope`. - */ -static void -apply_breadcrumbs_from_ring_files(sentry_value_t event, - const sentry_path_t *run_folder, const sentry_crash_context_t *ctx) -{ - if (ctx && ctx->max_breadcrumbs == 0) { - return; - } - - sentry_value_t b1 - = read_breadcrumb_ring_file(run_folder, "__sentry-breadcrumb1"); - sentry_value_t b2 - = read_breadcrumb_ring_file(run_folder, "__sentry-breadcrumb2"); - size_t max = ctx && ctx->max_breadcrumbs ? ctx->max_breadcrumbs - : SENTRY_BREADCRUMBS_MAX; - sentry_value_t merged = sentry__value_merge_breadcrumbs(b1, b2, max); - sentry_value_decref(b1); - sentry_value_decref(b2); - // Overwrite any breadcrumbs the base event may carry: the ring files are - // the single source of truth, so this is idempotent and never duplicates. - if (sentry_value_get_type(merged) == SENTRY_VALUE_TYPE_LIST) { - sentry_value_set_by_key(event, "breadcrumbs", merged); - } else { - sentry_value_decref(merged); - } -} - #if defined(SENTRY_PLATFORM_WINDOWS) static bool is_wer_done(const sentry_crash_context_t *ctx) @@ -3215,40 +3151,18 @@ enrich_stacktrace(const sentry_crash_context_t *ctx, sentry_value_t stacktrace) * Build a native event and set the level, mechanism, and handled state * * @param ctx Crash context - * @param event_file_path Path to base event file from parent process - * @param run_folder Run directory holding the breadcrumb ring files + * @param base Staged event received from the parent process * @param level Event level (e.g. "fatal") * @param mechanism_type Exception mechanism type (e.g. "signalhandler") * @param handled Whether the mechanism was handled */ static sentry_value_t -build_native_event(const sentry_crash_context_t *ctx, - const char *event_file_path, const sentry_path_t *run_folder, +build_native_event(const sentry_crash_context_t *ctx, sentry_value_t base, const char *level, const char *mechanism_type, bool handled) { - // Read base event from parent's file - sentry_value_t event = sentry_value_new_null(); - if (!sentry__string_empty(event_file_path)) { - sentry_path_t *ev_path = sentry__path_from_str(event_file_path); - if (ev_path) { - size_t event_size = 0; - char *event_json - = sentry__path_read_to_buffer(ev_path, &event_size); - sentry__path_free(ev_path); - if (event_json && event_size > 0) { - event = sentry_value_from_json(event_json, event_size); - sentry_free(event_json); - } - } - } + sentry_value_t event = sentry__value_clone(base); + sentry__ensure_event_id(event, NULL); - if (sentry_value_is_null(event)) { - event = sentry_value_new_event(); - } else { - sentry__ensure_event_id(event, NULL); - } - - apply_breadcrumbs_from_ring_files(event, run_folder, ctx); #if defined(SENTRY_PLATFORM_WINDOWS) apply_wer_context(event, ctx); #endif @@ -3583,14 +3497,14 @@ build_native_event(const sentry_crash_context_t *ctx, static bool write_envelope_with_native_stacktrace(const sentry_options_t *options, const char *envelope_path, const sentry_crash_context_t *ctx, - const char *event_file_path, const char *minidump_path, - sentry_path_t *run_folder) + sentry_value_t base, const char *minidump_path, sentry_path_t *run_folder, + sentry_value_t attachments) { // Build native crash event (always include threads with names) SENTRY_DEBUGF("write_envelope_with_native_stacktrace: minidump_path=%s", minidump_path ? minidump_path : "(null)"); - sentry_value_t event = build_native_event( - ctx, event_file_path, run_folder, "fatal", "signalhandler", false); + sentry_value_t event + = build_native_event(ctx, base, "fatal", "signalhandler", false); // Serialize event to JSON size_t event_size = 0; @@ -3746,9 +3660,27 @@ write_envelope_with_native_stacktrace(const sentry_options_t *options, } } - // Add scope attachments using metadata file - if (run_folder) { - write_attachments_from_manifest(fd, options, run_folder); + size_t len = sentry_value_get_length(attachments); + for (size_t i = 0; i < len; i++) { + sentry_value_t attach_info = sentry_value_get_by_index(attachments, i); + sentry_value_t path_val = sentry_value_get_by_key(attach_info, "path"); + sentry_value_t filename_val + = sentry_value_get_by_key(attach_info, "filename"); + sentry_value_t attachment_type_val + = sentry_value_get_by_key(attach_info, "attachment_type"); + sentry_value_t content_type_val + = sentry_value_get_by_key(attach_info, "content_type"); + + const char *path = sentry_value_as_string(path_val); + const char *filename = sentry_value_as_string(filename_val); + const char *attachment_type + = sentry_value_as_string(attachment_type_val); + const char *content_type = sentry_value_as_string(content_type_val); + + if (path && filename && !attachment_is_placeholder(options, path)) { + write_attachment_to_envelope( + fd, path, filename, attachment_type, content_type); + } } // Add screenshot attachment if captured by the daemon @@ -3789,48 +3721,21 @@ write_envelope_with_native_stacktrace(const sentry_options_t *options, static bool write_envelope_with_minidump(const sentry_options_t *options, const sentry_crash_context_t *ctx, const char *envelope_path, - const char *event_msgpack_path, const char *minidump_path, - sentry_path_t *run_folder) + sentry_value_t base, const char *minidump_path, sentry_path_t *run_folder, + sentry_value_t attachments) { - // Read the base event, merge in the breadcrumbs from the ring files, - // re-serialize. - size_t event_size = 0; - char *event_json = NULL; - char *event_id = NULL; - sentry_path_t *ev_path = sentry__path_from_str(event_msgpack_path); - if (ev_path) { - size_t base_size = 0; - char *base_json = sentry__path_read_to_buffer(ev_path, &base_size); - sentry__path_free(ev_path); - if (base_json && base_size > 0) { - sentry_value_t event = sentry_value_from_json(base_json, base_size); - if (sentry_value_is_null(event)) { - // Parsing the base event failed (e.g. truncated buffer or - // OOM). Don't serialize the null into "null" and ship an - // invalid payload - fall back to streaming the raw event - // bytes verbatim so the crash report is preserved. - sentry_value_decref(event); - event_json = sentry__string_clone_n(base_json, base_size); - event_size = event_json ? base_size : 0; - } else { - apply_breadcrumbs_from_ring_files(event, run_folder, ctx); + sentry_value_t event = sentry__value_clone(base); #if defined(SENTRY_PLATFORM_WINDOWS) - apply_wer_context(event, ctx); + apply_wer_context(event, ctx); #endif - event_id = sentry__string_clone(sentry_value_as_string( - sentry_value_get_by_key(event, "event_id"))); - event_json = sentry__value_to_json(event, &event_size); - sentry_value_decref(event); - if (!event_json) { - // Re-serialization failed (e.g. OOM). Fall back to the raw - // event bytes so the crash report is preserved, losing only - // the merged breadcrumbs rather than the whole event. - event_json = sentry__string_clone_n(base_json, base_size); - event_size = event_json ? base_size : 0; - } - } - } - sentry_free(base_json); + size_t event_size = 0; + char *event_json = sentry__value_to_json(event, &event_size); + char *event_id = sentry__string_clone( + sentry_value_as_string(sentry_value_get_by_key(event, "event_id"))); + sentry_value_decref(event); + if (!event_json) { + sentry_free(event_id); + return false; } // Open envelope file for writing @@ -3975,9 +3880,27 @@ write_envelope_with_minidump(const sentry_options_t *options, #endif } - // Add scope attachments using metadata file - if (run_folder) { - write_attachments_from_manifest(fd, options, run_folder); + size_t len = sentry_value_get_length(attachments); + for (size_t i = 0; i < len; i++) { + sentry_value_t attach_info = sentry_value_get_by_index(attachments, i); + sentry_value_t path_val = sentry_value_get_by_key(attach_info, "path"); + sentry_value_t filename_val + = sentry_value_get_by_key(attach_info, "filename"); + sentry_value_t attachment_type_val + = sentry_value_get_by_key(attach_info, "attachment_type"); + sentry_value_t content_type_val + = sentry_value_get_by_key(attach_info, "content_type"); + + const char *path = sentry_value_as_string(path_val); + const char *filename = sentry_value_as_string(filename_val); + const char *attachment_type + = sentry_value_as_string(attachment_type_val); + const char *content_type = sentry_value_as_string(content_type_val); + + if (path && filename && !attachment_is_placeholder(options, path)) { + write_attachment_to_envelope( + fd, path, filename, attachment_type, content_type); + } } // Add screenshot attachment if captured by the daemon @@ -4017,6 +3940,39 @@ write_envelope_with_minidump(const sentry_options_t *options, * * Called by the crash daemon (out-of-process on Linux/macOS). */ +#if defined(SENTRY_PLATFORM_WINDOWS) +// Sentry's symbolicator needs `contexts.device.arch` to process PE modules. If +// the scope already carries a device context with arch (host SDKs like Unity +// provide one), leave it; otherwise synthesize a minimal one so native-only +// consumers still symbolicate. +static void +ensure_device_arch(sentry_value_t event) +{ + sentry_value_t contexts = sentry_value_get_by_key(event, "contexts"); + if (sentry_value_is_null(contexts)) { + contexts = sentry_value_new_object(); + sentry_value_set_by_key(event, "contexts", contexts); + } + sentry_value_t device = sentry_value_get_by_key(contexts, "device"); + if (sentry_value_is_null(device)) { + device = sentry_value_new_object(); + sentry_value_set_by_key( + device, "type", sentry_value_new_string("device")); + sentry_value_set_by_key(contexts, "device", device); + } + if (!sentry_value_is_null(sentry_value_get_by_key(device, "arch"))) { + return; + } +# if defined(_M_AMD64) + sentry_value_set_by_key(device, "arch", sentry_value_new_string("x86_64")); +# elif defined(_M_IX86) + sentry_value_set_by_key(device, "arch", sentry_value_new_string("x86")); +# elif defined(_M_ARM64) + sentry_value_set_by_key(device, "arch", sentry_value_new_string("arm64")); +# endif +} +#endif + bool sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) { @@ -4028,7 +3984,7 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) // Mark as processing sentry__atomic_store(&ctx->state, SENTRY_CRASH_STATE_PROCESSING); SENTRY_DEBUG("Marked state as PROCESSING"); - if (!ctx->event_path[0]) { + if (ctx->event_discarded) { SENTRY_DEBUG("Crash event discarded by local callback"); goto done; } @@ -4100,33 +4056,23 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) goto done; } - // Get event file path from context - const char *event_path = ctx->event_path[0] ? ctx->event_path : NULL; - SENTRY_DEBUGF( - "Event path from context: %s", event_path ? event_path : "(null)"); - if (!event_path) { - SENTRY_WARN("No event file from parent"); - if (minidump_path[0]) { - // Delete the orphaned minidump to prevent disk space leaks -#if defined(SENTRY_PLATFORM_UNIX) - unlink(minidump_path); -#elif defined(SENTRY_PLATFORM_WINDOWS) - wchar_t *wpath = sentry__string_to_wstr(minidump_path); - if (wpath) { - _wunlink(wpath); - sentry_free(wpath); - } -#endif - } - ctx->minidump_path[0] = '\0'; - goto done; + sentry_value_t base = sentry__crash_scope_event(&ipc->scope); + sentry_path_t *run_folder = sentry__path_from_str(ctx->run_path); + sentry_value_t crash_event + = run_folder ? read_crash_event(run_folder) : sentry_value_new_null(); + if (!sentry_value_is_null(crash_event)) { + sentry_value_decref(base); + base = crash_event; } - - // Extract run folder path from event path (event is at - // run_folder/__sentry-event) - SENTRY_DEBUG("Extracting run folder from event path"); - sentry_path_t *ev_path = sentry__path_from_str(event_path); - sentry_path_t *run_folder = ev_path ? sentry__path_dir(ev_path) : NULL; + sentry_value_t attachments = run_folder + ? read_attachment_manifest(run_folder) + : sentry_value_new_null(); + if (sentry_value_is_null(attachments)) { + attachments = sentry_value_incref(ipc->scope.attachments); + } +#if defined(SENTRY_PLATFORM_WINDOWS) + ensure_device_arch(base); +#endif // The crashing process dumps its pending logs, sessions, and transactions // before notifying the daemon. Queue those before writing the crash @@ -4144,7 +4090,8 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) if (path_len < 0 || path_len >= (int)sizeof(envelope_path)) { SENTRY_WARN("Envelope path truncated or invalid"); - sentry__path_free(ev_path); + sentry_value_decref(base); + sentry_value_decref(attachments); if (run_folder) { sentry__path_free(run_folder); } @@ -4253,18 +4200,19 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) "minidump_path=%s", minidump_path[0] ? minidump_path : "NULL"); envelope_written = write_envelope_with_native_stacktrace(options, - envelope_path, ctx, event_path, - minidump_path[0] ? minidump_path : NULL, run_folder); + envelope_path, ctx, base, minidump_path[0] ? minidump_path : NULL, + run_folder, attachments); } else { // Mode 0 (MINIDUMP only) SENTRY_DEBUG("Writing envelope with minidump"); - envelope_written = write_envelope_with_minidump( - options, ctx, envelope_path, event_path, minidump_path, run_folder); + envelope_written = write_envelope_with_minidump(options, ctx, + envelope_path, base, minidump_path, run_folder, attachments); } if (!envelope_written) { SENTRY_WARN("Failed to write envelope"); - sentry__path_free(ev_path); + sentry_value_decref(base); + sentry_value_decref(attachments); if (run_folder) { sentry__path_free(run_folder); } @@ -4314,7 +4262,7 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) goto cleanup; } - add_attachment_refs(envelope, options, run_folder); + add_attachment_refs(envelope, options, attachments); bool has_attachment_refs = sentry__envelope_has_content_type( envelope, SENTRY_ATTACHMENT_REF_MIME); @@ -4356,36 +4304,15 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) } cleanup: - // Send the staged session-replay envelope same-session, enriched from the - // crash event (`/__sentry-event`) so it shares the crash's - // tags/contexts/trace and embeds its breadcrumbs. Only flush when the - // crash itself was delivered: `cleanup` is also reached via `goto` on - // error paths where the crash was never captured, and flushing there - // would consume (and delete) the staged replay for a crash that never - // arrived. + // flush replay only after the crash itself was captured if (crash_captured && options && options->transport && sentry__session_replay_has_pending(options)) { - sentry_value_t crash_event = sentry_value_new_null(); - if (ev_path) { - size_t ev_len = 0; - char *ev_json = sentry__path_read_to_buffer(ev_path, &ev_len); - if (ev_json) { - crash_event = sentry_value_from_json(ev_json, ev_len); - sentry_free(ev_json); - } - } - if (!sentry_value_is_null(crash_event)) { - // `__sentry-event` is scope-applied without breadcrumbs; merge - // the ring files so the replay recording can embed them - apply_breadcrumbs_from_ring_files(crash_event, run_folder, ctx); - } - sentry__session_replay_flush_pending( - options, options->transport, crash_event); - sentry_value_decref(crash_event); + sentry__session_replay_flush_pending(options, options->transport, base); } sentry__path_free(run_folder); - sentry__path_free(ev_path); + sentry_value_decref(base); + sentry_value_decref(attachments); SENTRY_DEBUG("Crash processing completed successfully"); @@ -4662,6 +4589,16 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, ipc->parent_handle = app_pid; #endif + if (!sentry__crash_scope_init(&ipc->scope, ipc->shmem->max_breadcrumbs)) { + sentry__crash_ipc_free(ipc); + sentry_options_free(options); + if (log_file) { + sentry__logger_disable(); + fclose(log_file); + } + return 1; + } + // Signal to parent that daemon is ready SENTRY_DEBUG("Signaling ready to parent"); sentry__crash_ipc_signal_ready(ipc); @@ -4690,8 +4627,8 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, uint64_t crash_deadline = 0; while (true) { // Wait for crash notification (with timeout to check parent health) - bool wait_result - = sentry__crash_ipc_wait(ipc, SENTRY_CRASH_DAEMON_WAIT_TIMEOUT_MS); + bool wait_result = sentry__crash_ipc_receive( + ipc, SENTRY_CRASH_DAEMON_WAIT_TIMEOUT_MS); if (wait_result) { SENTRY_DEBUG("Event signaled, checking crash state"); } @@ -4746,8 +4683,17 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, SENTRY_DEBUG("Spurious notification or already processed"); } + if (ipc->scope.stopped && state != SENTRY_CRASH_STATE_CRASHING) { + break; + } + if (ipc->message_closed && parent_alive) { + // retain legacy WER notifications after the app closes its stream + ipc->notify_pending = sentry__crash_ipc_wait( + ipc, SENTRY_CRASH_DAEMON_WAIT_TIMEOUT_MS); + } + // Check if parent is still alive (only if no crash processed yet) - if (!parent_alive) { + if (!parent_alive && !ipc->notify_pending) { SENTRY_DEBUG("Parent process exited without crash"); break; } diff --git a/src/backends/native/sentry_crash_ipc.c b/src/backends/native/sentry_crash_ipc.c index 94cdd0eef4..fb123ffd21 100644 --- a/src/backends/native/sentry_crash_ipc.c +++ b/src/backends/native/sentry_crash_ipc.c @@ -1,12 +1,18 @@ #include "sentry_crash_ipc.h" #include "sentry_alloc.h" +#include "sentry_core.h" #include "sentry_logger.h" #include "sentry_sync.h" +#include "sentry_value.h" #include #include +#if defined(SENTRY_PLATFORM_UNIX) +# include +#endif + static void write_u16_le(char *buf, uint16_t value) { @@ -114,6 +120,9 @@ sentry__crash_ipc_message_decode( } uint32_t message_len = read_u32_le(buf); + if (message_len > SENTRY_CRASH_IPC_MESSAGE_MAX_LEN) { + return SENTRY_CRASH_IPC_MESSAGE_OVERSIZED; + } if (message_len < SENTRY_CRASH_IPC_MESSAGE_MIN_LEN) { return SENTRY_CRASH_IPC_MESSAGE_INVALID; } @@ -309,6 +318,10 @@ sentry__crash_ipc_init_app(sem_t *init_sem) return NULL; } + struct timeval send_timeout = { 1, 0 }; + setsockopt(ipc->message_fd[0], SOL_SOCKET, SO_SNDTIMEO, &send_timeout, + sizeof(send_timeout)); + // Initialize shared memory only if newly created if (!shm_exists) { sentry__crash_context_init(ipc->shmem); @@ -336,6 +349,7 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, ipc->is_daemon = true; ipc->message_fd[0] = -1; ipc->message_fd[1] = message_fd; + fcntl(message_fd, F_SETFD, FD_CLOEXEC); // Open existing shared memory created by app (using PID and thread ID) // Must match the format in sentry__crash_ipc_init_app @@ -381,8 +395,8 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, return ipc; } -void -sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc) +static void +notify_fallback(sentry_crash_ipc_t *ipc) { if (!ipc || ipc->notify_fd < 0) { return; @@ -440,6 +454,9 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) return; } + sentry_free(ipc->message_buf); + sentry__crash_scope_free(&ipc->scope); + if (ipc->shmem && ipc->shmem != MAP_FAILED) { munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); } @@ -634,6 +651,12 @@ sentry__crash_ipc_init_app(sentry_mutex_t *init_mutex) sentry_free(ipc); return NULL; } + int no_sigpipe = 1; + setsockopt(ipc->message_fd[0], SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, + sizeof(no_sigpipe)); + struct timeval send_timeout = { 1, 0 }; + setsockopt(ipc->message_fd[0], SOL_SOCKET, SO_SNDTIMEO, &send_timeout, + sizeof(send_timeout)); fcntl(ipc->message_fd[0], F_SETFD, FD_CLOEXEC); fcntl(ipc->message_fd[1], F_SETFD, FD_CLOEXEC); @@ -665,6 +688,7 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, ipc->is_daemon = true; ipc->message_fd[0] = -1; ipc->message_fd[1] = message_fd; + fcntl(message_fd, F_SETFD, FD_CLOEXEC); // Use the inherited shm_fd directly (no shm_open needed, sandbox-safe) ipc->shm_fd = shm_fd; @@ -699,8 +723,8 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, return ipc; } -void -sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc) +static void +notify_fallback(sentry_crash_ipc_t *ipc) { if (!ipc) { return; @@ -754,6 +778,9 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) return; } + sentry_free(ipc->message_buf); + sentry__crash_scope_free(&ipc->scope); + if (ipc->shmem && ipc->shmem != MAP_FAILED) { munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); } @@ -947,6 +974,7 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, } ipc->is_daemon = true; ipc->message_read_handle = message_read_handle; + SetHandleInformation(message_read_handle, HANDLE_FLAG_INHERIT, 0); // Open existing shared memory (using PID and thread ID) swprintf(ipc->shm_name, SENTRY_CRASH_IPC_NAME_SIZE, @@ -1012,8 +1040,8 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, return ipc; } -void -sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc) +static void +notify_fallback(sentry_crash_ipc_t *ipc) { if (!ipc || !ipc->event_handle) { // No logging - called from signal handler/exception filter @@ -1060,6 +1088,9 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) return; } + sentry_free(ipc->message_buf); + sentry__crash_scope_free(&ipc->scope); + if (ipc->shmem) { UnmapViewOfFile(ipc->shmem); } @@ -1215,3 +1246,445 @@ sentry__crash_ipc_wait_for_ready(sentry_crash_ipc_t *ipc, int timeout_ms) return false; #endif } + +bool +sentry__crash_scope_init(sentry_crash_scope_t *scope, size_t max_breadcrumbs) +{ + memset(scope, 0, sizeof(*scope)); + scope->event = sentry_value_new_null(); + scope->attachments = sentry_value_new_list(); + scope->breadcrumbs = sentry__ringbuffer_new(max_breadcrumbs); + return scope->breadcrumbs && !sentry_value_is_null(scope->attachments); +} + +void +sentry__crash_scope_free(sentry_crash_scope_t *scope) +{ + sentry_value_decref(scope->event); + sentry_value_decref(scope->attachments); + sentry__ringbuffer_free(scope->breadcrumbs); +} + +bool +sentry__crash_scope_apply( + sentry_crash_scope_t *scope, const sentry_crash_ipc_message_t *message) +{ + if (scope->stopped || message->flags + || message->sequence != scope->sequence + 1 + || (!scope->initialized + && message->type != SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT)) { + return false; + } + if (message->type == SENTRY_CRASH_IPC_MESSAGE_CRASH + || message->type == SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN) { + if (message->payload_len) { + return false; + } + scope->sequence = message->sequence; + scope->stopped = true; + return true; + } + + sentry_value_t value + = sentry_value_from_msgpack(message->payload, message->payload_len); + sentry_value_type_t type = sentry_value_get_type(value); + if (sentry_value_is_null(value) + && (message->payload_len != 1 + || (unsigned char)message->payload[0] != 0xc0)) { + return false; + } + bool valid = false; + const char *field = NULL; + switch (message->type) { + case SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT: { + sentry_value_t event = sentry_value_get_by_key(value, "event"); + sentry_value_t attachments + = sentry_value_get_by_key(value, "attachments"); + if (sentry_value_get_type(event) != SENTRY_VALUE_TYPE_OBJECT + || sentry_value_get_type(attachments) != SENTRY_VALUE_TYPE_LIST) { + break; + } + sentry_ringbuffer_t *breadcrumbs + = sentry__ringbuffer_new(scope->breadcrumbs->max_size); + if (!breadcrumbs) { + break; + } + sentry_value_t list = sentry_value_get_by_key(event, "breadcrumbs"); + for (size_t i = 0; i < sentry_value_get_length(list); i++) { + sentry__ringbuffer_append(breadcrumbs, + sentry_value_incref(sentry_value_get_by_index(list, i))); + } + sentry_value_remove_by_key(event, "breadcrumbs"); + sentry__value_replace(&scope->event, sentry_value_incref(event)); + sentry__value_replace( + &scope->attachments, sentry_value_incref(attachments)); + sentry__ringbuffer_free(scope->breadcrumbs); + scope->breadcrumbs = breadcrumbs; + scope->initialized = true; + valid = true; + break; + } + case SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE: + field = "release"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_ENVIRONMENT: + field = "environment"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_TRANSACTION: + field = "transaction"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT: + field = "fingerprint"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_LEVEL: + field = "level"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_USER: + field = "user"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_TAG: + case SENTRY_CRASH_IPC_MESSAGE_SET_EXTRA: + case SENTRY_CRASH_IPC_MESSAGE_SET_CONTEXT: { + const char *key + = sentry_value_as_string(sentry_value_get_by_index(value, 0)); + if (type != SENTRY_VALUE_TYPE_LIST + || sentry_value_get_length(value) != 2 + || sentry_value_get_type(sentry_value_get_by_index(value, 0)) + != SENTRY_VALUE_TYPE_STRING) { + break; + } + const char *name = message->type == SENTRY_CRASH_IPC_MESSAGE_SET_TAG + ? "tags" + : message->type == SENTRY_CRASH_IPC_MESSAGE_SET_EXTRA ? "extra" + : "contexts"; + sentry_value_t object = sentry_value_get_by_key(scope->event, name); + if (sentry_value_is_null(object)) { + object = sentry_value_new_object(); + sentry_value_set_by_key(scope->event, name, object); + } + valid + = sentry_value_set_by_key_n(object, key, + sentry_value_get_length(sentry_value_get_by_index(value, 0)), + sentry_value_incref(sentry_value_get_by_index(value, 1))) + == 0; + break; + } + case SENTRY_CRASH_IPC_MESSAGE_REMOVE_TAG: + case SENTRY_CRASH_IPC_MESSAGE_REMOVE_EXTRA: + case SENTRY_CRASH_IPC_MESSAGE_REMOVE_CONTEXT: { + if (type != SENTRY_VALUE_TYPE_STRING) { + break; + } + const char *name = message->type == SENTRY_CRASH_IPC_MESSAGE_REMOVE_TAG + ? "tags" + : message->type == SENTRY_CRASH_IPC_MESSAGE_REMOVE_EXTRA + ? "extra" + : "contexts"; + sentry_value_remove_by_key_n( + sentry_value_get_by_key(scope->event, name), + sentry_value_as_string(value), sentry_value_get_length(value)); + valid = true; + break; + } + case SENTRY_CRASH_IPC_MESSAGE_ADD_BREADCRUMB: + if (type == SENTRY_VALUE_TYPE_OBJECT) { + valid = !scope->breadcrumbs->max_size + || sentry__ringbuffer_append( + scope->breadcrumbs, sentry_value_incref(value)) + == 0; + } + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST: + if (type == SENTRY_VALUE_TYPE_LIST) { + sentry__value_replace( + &scope->attachments, sentry_value_incref(value)); + valid = true; + } + break; + default: + break; + } + if (field) { + sentry_value_type_t expected + = message->type == SENTRY_CRASH_IPC_MESSAGE_SET_USER + ? SENTRY_VALUE_TYPE_OBJECT + : message->type == SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT + ? SENTRY_VALUE_TYPE_LIST + : SENTRY_VALUE_TYPE_STRING; + if (type == expected || sentry_value_is_null(value)) { + if (sentry_value_is_null(value) + || (type == SENTRY_VALUE_TYPE_STRING + && !sentry_value_get_length(value))) { + sentry_value_remove_by_key(scope->event, field); + valid = true; + } else { + valid = sentry_value_set_by_key( + scope->event, field, sentry_value_incref(value)) + == 0; + } + } + } + sentry_value_decref(value); + if (valid) { + scope->sequence = message->sequence; + } + return valid; +} + +sentry_value_t +sentry__crash_scope_event(const sentry_crash_scope_t *scope) +{ + sentry_value_t event = scope->initialized + ? sentry__value_clone(scope->event) + : sentry_value_new_event(); + sentry_value_set_by_key( + event, "breadcrumbs", sentry__ringbuffer_to_list(scope->breadcrumbs)); + sentry_value_set_by_key( + event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); + sentry__ensure_event_id(event, NULL); + return event; +} + +static bool +write_message( + sentry_crash_ipc_t *ipc, const char *buf, size_t len, bool nonblocking) +{ + while (len) { +#if defined(SENTRY_PLATFORM_WINDOWS) + (void)nonblocking; + DWORD written = 0; + if (!WriteFile( + ipc->message_write_handle, buf, (DWORD)len, &written, NULL) + || !written) { + return false; + } +#else + int flags = nonblocking ? MSG_DONTWAIT : 0; +# if defined(MSG_NOSIGNAL) + flags |= MSG_NOSIGNAL; +# endif + ssize_t written = send(ipc->message_fd[0], buf, len, flags); + if (written < 0 && errno == EINTR) { + continue; + } + if (written <= 0) { + return false; + } +#endif + buf += written; + len -= (size_t)written; + } + return true; +} + +bool +sentry__crash_ipc_send( + sentry_crash_ipc_t *ipc, uint16_t type, sentry_value_t value) +{ + if (!ipc || sentry__atomic_fetch(&ipc->stopped) + || sentry__atomic_fetch(&ipc->message_failed) + || !sentry__atomic_compare_swap(&ipc->writing, 0, 1)) { + return false; + } + size_t payload_len = 0; + char *payload = type == SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN + ? NULL + : sentry_value_to_msgpack(value, &payload_len); + char *buf = NULL; + size_t len = 0; + bool sent = (payload || type == SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN) + && sentry__crash_ipc_message_encode( + type, 0, ipc->sequence + 1, payload, payload_len, &buf, &len) + == SENTRY_CRASH_IPC_MESSAGE_OK; + if (sent) { + sent = write_message(ipc, buf, len, false); + if (sent) { + ipc->sequence++; + } else { + // a partial write makes this stream unusable for further frames + sentry__atomic_store(&ipc->message_failed, 1); + } + } + sentry_free(buf); + sentry_free(payload); + sentry__atomic_store(&ipc->writing, 0); + return sent; +} + +void +sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc) +{ + if (!ipc) { + return; + } + if (sentry__atomic_fetch(&ipc->shmem->state) + == SENTRY_CRASH_STATE_CRASHING) { + notify_fallback(ipc); + return; + } + sentry__atomic_store(&ipc->stopped, 1); +#if !defined(SENTRY_PLATFORM_WINDOWS) + if (!sentry__atomic_fetch(&ipc->message_failed) + && sentry__atomic_compare_swap(&ipc->writing, 0, 1)) { + char frame[SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE] = { 0 }; + write_u32_le(frame, SENTRY_CRASH_IPC_MESSAGE_MIN_LEN); + write_u16_le(frame + 4, SENTRY_CRASH_IPC_MESSAGE_CRASH); + write_u64_le(frame + 8, ipc->sequence + 1); + bool sent = write_message(ipc, frame, sizeof(frame), true); + sentry__atomic_store(&ipc->writing, 0); + if (sent) { + return; + } + } +#endif + // also used by WER; the daemon drains complete frames before freezing + notify_fallback(ipc); +} + +static void +close_message_stream(sentry_crash_ipc_t *ipc) +{ + ipc->message_closed = true; +#if defined(SENTRY_PLATFORM_WINDOWS) + CloseHandle(ipc->message_read_handle); + ipc->message_read_handle = NULL; +#else + close(ipc->message_fd[1]); + ipc->message_fd[1] = -1; +#endif +} + +bool +sentry__crash_ipc_receive(sentry_crash_ipc_t *ipc, int timeout_ms) +{ + // a bounded poll also checks legacy notifications without blocking a writer + int remaining = timeout_ms; + do { + ipc->notify_pending + = ipc->notify_pending || sentry__crash_ipc_wait(ipc, 0); + bool available = false; + if (!ipc->message_closed) { +#if defined(SENTRY_PLATFORM_WINDOWS) + DWORD bytes = 0; + if (!PeekNamedPipe( + ipc->message_read_handle, NULL, 0, NULL, &bytes, NULL)) { + close_message_stream(ipc); + } + available = bytes > 0; +#else + struct pollfd fd = { ipc->message_fd[1], POLLIN, 0 }; + available = poll(&fd, 1, 0) > 0; +#endif + } + if (!available) { + if (ipc->notify_pending) { + if (sentry__atomic_fetch(&ipc->shmem->state) + == SENTRY_CRASH_STATE_CRASHING) { + ipc->notify_pending = false; + } else { + ipc->scope.stopped = true; + } + return true; + } + if (ipc->message_closed || remaining <= 0) { + return false; + } +#if defined(SENTRY_PLATFORM_WINDOWS) + int delay = remaining < 10 ? remaining : 10; + ipc->notify_pending = sentry__crash_ipc_wait(ipc, delay); + remaining -= delay; +#else +# if defined(SENTRY_PLATFORM_MACOS) + int notify_fd = ipc->notify_pipe[0]; +# else + int notify_fd = ipc->notify_fd; +# endif + struct pollfd fds[] = { + { ipc->message_fd[1], POLLIN, 0 }, + { notify_fd, POLLIN, 0 }, + }; + int rv = poll(fds, 2, remaining); + if (!rv || (rv < 0 && errno != EINTR)) { + return false; + } +#endif + continue; + } + if (!ipc->message_buf) { + ipc->message_size = SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE; + ipc->message_buf = sentry_malloc(ipc->message_size); + if (!ipc->message_buf) { + close_message_stream(ipc); + continue; + } + } + size_t len = ipc->message_size - ipc->message_len; +#if defined(SENTRY_PLATFORM_WINDOWS) + DWORD bytes = 0, received = 0; + if (!PeekNamedPipe( + ipc->message_read_handle, NULL, 0, NULL, &bytes, NULL) + || !ReadFile(ipc->message_read_handle, + ipc->message_buf + ipc->message_len, + (DWORD)(len < bytes ? len : bytes), &received, NULL) + || !received) { + close_message_stream(ipc); + continue; + } +#else + ssize_t received = recv(ipc->message_fd[1], + ipc->message_buf + ipc->message_len, len, MSG_DONTWAIT); + if (received < 0 && (errno == EINTR || errno == EAGAIN)) { + continue; + } + if (received <= 0) { + close_message_stream(ipc); + continue; + } +#endif + ipc->message_len += (size_t)received; + if (ipc->message_len < ipc->message_size) { + continue; + } + sentry_crash_ipc_message_t message; + sentry_crash_ipc_message_result_t result + = sentry__crash_ipc_message_decode( + ipc->message_buf, ipc->message_len, &message); + if (result == SENTRY_CRASH_IPC_MESSAGE_PARTIAL) { + if (!sentry__crash_ipc_message_type_is_known( + read_u16_le(ipc->message_buf + 4)) + || read_u16_le(ipc->message_buf + 6) + || read_u64_le(ipc->message_buf + 8) + != ipc->scope.sequence + 1) { + close_message_stream(ipc); + continue; + } + ipc->message_size = (size_t)read_u32_le(ipc->message_buf) + + SENTRY_CRASH_IPC_MESSAGE_PREFIX_SIZE; + char *buf = sentry_malloc(ipc->message_size); + if (!buf) { + close_message_stream(ipc); + continue; + } + memcpy(buf, ipc->message_buf, ipc->message_len); + sentry_free(ipc->message_buf); + ipc->message_buf = buf; + continue; + } + if (result != SENTRY_CRASH_IPC_MESSAGE_OK + || !sentry__crash_scope_apply(&ipc->scope, &message)) { + SENTRY_WARN("daemon: invalid scope IPC frame"); + close_message_stream(ipc); + continue; + } + uint16_t type = message.type; + sentry_free(ipc->message_buf); + ipc->message_buf = NULL; + ipc->message_len = 0; + if (type == SENTRY_CRASH_IPC_MESSAGE_CRASH) { + return true; + } + if (type == SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN) { + return false; + } + } while (true); +} diff --git a/src/backends/native/sentry_crash_ipc.h b/src/backends/native/sentry_crash_ipc.h index dbefd8b3b8..6704dbc600 100644 --- a/src/backends/native/sentry_crash_ipc.h +++ b/src/backends/native/sentry_crash_ipc.h @@ -3,6 +3,7 @@ #include "sentry_boot.h" #include "sentry_crash_context.h" +#include "sentry_ringbuffer.h" #include #include @@ -22,8 +23,14 @@ #define SENTRY_CRASH_IPC_MESSAGE_PREFIX_SIZE 4u #define SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE 16u #define SENTRY_CRASH_IPC_MESSAGE_MIN_LEN 12u -#define SENTRY_CRASH_IPC_MESSAGE_MAX_LEN UINT32_MAX +#define SENTRY_CRASH_IPC_MESSAGE_MAX_LEN (256u * 1024u * 1024u) +/** + * Frames use little-endian length/type/flags/sequence, followed by one msgpack + * value. Flags are reserved (zero). Snapshots carry {event, attachments}; keyed + * setters carry [key, value], removals carry the key, and other updates carry + * the new value. Crash and shutdown frames have no payload. + */ typedef enum { SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT = 1, SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE = 2, @@ -70,6 +77,22 @@ sentry_crash_ipc_message_result_t sentry__crash_ipc_message_encode( sentry_crash_ipc_message_result_t sentry__crash_ipc_message_decode( const char *buf, size_t buf_len, sentry_crash_ipc_message_t *message); +typedef struct { + sentry_value_t event; + sentry_value_t attachments; + sentry_ringbuffer_t *breadcrumbs; + uint64_t sequence; + bool initialized; + bool stopped; +} sentry_crash_scope_t; + +bool sentry__crash_scope_init( + sentry_crash_scope_t *scope, size_t max_breadcrumbs); +void sentry__crash_scope_free(sentry_crash_scope_t *scope); +bool sentry__crash_scope_apply( + sentry_crash_scope_t *scope, const sentry_crash_ipc_message_t *message); +sentry_value_t sentry__crash_scope_event(const sentry_crash_scope_t *scope); + #if defined(SENTRY_PLATFORM_WINDOWS) typedef HANDLE sentry_process_handle_t; #else @@ -82,6 +105,20 @@ typedef pid_t sentry_process_handle_t; typedef struct { sentry_crash_context_t *shmem; + // app-only writer guard; crash handling must never wait for its owner + volatile long writing; + volatile long message_failed; + volatile long stopped; + uint64_t sequence; + + // daemon-only staged state and incremental frame reader + sentry_crash_scope_t scope; + char *message_buf; + size_t message_len; + size_t message_size; + bool message_closed; + bool notify_pending; + #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) int shm_fd; int notify_fd; // Eventfd for crash notifications @@ -182,6 +219,21 @@ void sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc); */ bool sentry__crash_ipc_wait(sentry_crash_ipc_t *ipc, int timeout_ms); +/** + * Send a borrowed value. Scope updates must hold the scope observer lock. + * Returns false on failure or if a crash interrupted another writer. + */ +bool sentry__crash_ipc_send( + sentry_crash_ipc_t *ipc, uint16_t type, sentry_value_t value); + +/** + * Receive and apply ordered frames, returning true on a crash notification. + * Early crash notifications keep the staged scope mutable. + * An out-of-band notification drains complete available frames first; an + * interrupted partial frame is discarded without changing staged state. + */ +bool sentry__crash_ipc_receive(sentry_crash_ipc_t *ipc, int timeout_ms); + /** * Unlink the shared memory. */ diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 64dc467dc6..8291aae52d 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -207,44 +207,17 @@ typedef struct { sentry_crash_ipc_t *ipc; pid_t daemon_pid; sentry_path_t *run_path; - sentry_path_t *event_path; - sentry_path_t *breadcrumb1_path; - sentry_path_t *breadcrumb2_path; sentry_path_t *envelope_path; - size_t max_breadcrumbs; - size_t num_breadcrumbs; - volatile long crashed; + const sentry_options_t *options; sentry_scope_observer_t *scope_observer; + volatile long crashed; } native_backend_state_t; -static void native_backend_flush_scope( - sentry_backend_t *backend, const sentry_options_t *options); -static void native_backend_add_breadcrumb( - void *data, sentry_value_t breadcrumb); -static void add_attachment(void *data, sentry_value_t attachment); - -static void -native_backend_preload_scope(sentry_backend_t *backend) -{ - sentry_value_t breadcrumbs = sentry_value_new_null(); - SENTRY_WITH_SCOPE (scope) { - sentry_value_t attachments = sentry__scope_load_attachments(scope); - size_t attachment_count = sentry_value_get_length(attachments); - for (size_t i = 0; i < attachment_count; i++) { - add_attachment( - backend->data, sentry_value_get_by_index(attachments, i)); - } - sentry_value_decref(attachments); - breadcrumbs = sentry__scope_breadcrumbs_to_list(scope); - } - - size_t breadcrumb_count = sentry_value_get_length(breadcrumbs); - for (size_t i = 0; i < breadcrumb_count; i++) { - native_backend_add_breadcrumb( - backend->data, sentry_value_get_by_index(breadcrumbs, i)); - } - sentry_value_decref(breadcrumbs); -} +static sentry_value_t attachment_list( + native_backend_state_t *state, const sentry_scope_t *scope); +static void sync_attachments(void *data, sentry_value_t attachment); +static bool observe_scope(native_backend_state_t *state); +static void native_backend_shutdown(sentry_backend_t *backend); static void add_attachment(void *data, sentry_value_t attachment) @@ -568,27 +541,48 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, } // Prepare process creation structures - STARTUPINFOW si; + STARTUPINFOEXW si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); + si.StartupInfo.cb = sizeof(si); // Hide console window for daemon - si.dwFlags = STARTF_USESHOWWINDOW; - si.wShowWindow = SW_HIDE; + si.StartupInfo.dwFlags = STARTF_USESHOWWINDOW; + si.StartupInfo.wShowWindow = SW_HIDE; ZeroMemory(&pi, sizeof(pi)); + SIZE_T attr_size = 0; + InitializeProcThreadAttributeList(NULL, 1, 0, &attr_size); + si.lpAttributeList = sentry_malloc(attr_size); + if (!si.lpAttributeList) { + return (pid_t)-1; + } + if (!InitializeProcThreadAttributeList( + si.lpAttributeList, 1, 0, &attr_size)) { + sentry_free(si.lpAttributeList); + return (pid_t)-1; + } + if (!UpdateProcThreadAttribute(si.lpAttributeList, 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &message_read_handle, + sizeof(message_read_handle), NULL, NULL)) { + DeleteProcThreadAttributeList(si.lpAttributeList); + sentry_free(si.lpAttributeList); + return (pid_t)-1; + } + // Create the daemon process if (!CreateProcessW(NULL, // Application name (use command line) cmd_line, // Command line NULL, // Process security attributes NULL, // Thread security attributes - TRUE, // Inherit handles (for event_handle) - CREATE_NO_WINDOW | DETACHED_PROCESS, // Creation flags + TRUE, // inherit the control-stream read handle + CREATE_NO_WINDOW | DETACHED_PROCESS | EXTENDED_STARTUPINFO_PRESENT, NULL, // Environment NULL, // Current directory - &si, // Startup info + &si.StartupInfo, // Startup info &pi)) { // Process information DWORD error = GetLastError(); + DeleteProcThreadAttributeList(si.lpAttributeList); + sentry_free(si.lpAttributeList); char *daemon_path_err = sentry__string_from_wstr(daemon_path_w); if (daemon_path_err) { SENTRY_WARNF("Failed to create daemon process at '%s': Error %lu%s", @@ -603,6 +597,9 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, return (pid_t)-1; } + DeleteProcThreadAttributeList(si.lpAttributeList); + sentry_free(si.lpAttributeList); + // Close thread handle (we don't need it) CloseHandle(pi.hThread); @@ -614,31 +611,6 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, #endif } -static void -clear_scope(void *data) -{ - native_backend_state_t *state = (native_backend_state_t *)data; - if (!state) { - return; - } - - const sentry_path_t *paths[] - = { state->breadcrumb1_path, state->breadcrumb2_path }; - for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) { - if (paths[i] && sentry__path_write_buffer(paths[i], "", 0) != 0) { - SENTRY_WARN("failed to clear persisted breadcrumbs"); - } - } - state->num_breadcrumbs = 0; - - sentry_path_t *path - = sentry__path_join_str(state->run_path, "__sentry-attachments"); - if (path && sentry__path_is_file(path) && sentry__path_remove(path) != 0) { - SENTRY_WARN("failed to clear attachment manifest"); - } - sentry__path_free(path); -} - static int native_backend_startup( sentry_backend_t *backend, const sentry_options_t *options) @@ -702,7 +674,7 @@ native_backend_startup( } backend->data = state; state->run_path = sentry__path_clone(options->run->run_path); - state->max_breadcrumbs = options->max_breadcrumbs; + state->options = options; // Initialize IPC (protected by global synchronization for concurrent // access) @@ -852,36 +824,6 @@ native_backend_startup( #endif } - state->event_path = sentry__path_join_str(run_path, "__sentry-event"); - state->breadcrumb1_path - = sentry__path_join_str(run_path, "__sentry-breadcrumb1"); - state->breadcrumb2_path - = sentry__path_join_str(run_path, "__sentry-breadcrumb2"); - - sentry__path_touch(state->event_path); - sentry__path_touch(state->breadcrumb1_path); - sentry__path_touch(state->breadcrumb2_path); - - // Copy paths to crash context -#ifdef _WIN32 - strncpy_s(ctx->event_path, sizeof(ctx->event_path), state->event_path->path, - _TRUNCATE); - strncpy_s(ctx->breadcrumb1_path, sizeof(ctx->breadcrumb1_path), - state->breadcrumb1_path->path, _TRUNCATE); - strncpy_s(ctx->breadcrumb2_path, sizeof(ctx->breadcrumb2_path), - state->breadcrumb2_path->path, _TRUNCATE); -#else - strncpy( - ctx->event_path, state->event_path->path, sizeof(ctx->event_path) - 1); - ctx->event_path[sizeof(ctx->event_path) - 1] = '\0'; - strncpy(ctx->breadcrumb1_path, state->breadcrumb1_path->path, - sizeof(ctx->breadcrumb1_path) - 1); - ctx->breadcrumb1_path[sizeof(ctx->breadcrumb1_path) - 1] = '\0'; - strncpy(ctx->breadcrumb2_path, state->breadcrumb2_path->path, - sizeof(ctx->breadcrumb2_path) - 1); - ctx->breadcrumb2_path[sizeof(ctx->breadcrumb2_path) - 1] = '\0'; -#endif - // Set up crash envelope path state->envelope_path = sentry__path_join_str( options->run->run_path, "__sentry-crash.envelope"); @@ -925,10 +867,6 @@ native_backend_startup( } #endif - // Persist the preloaded scope before any crash handler becomes active. - native_backend_preload_scope(backend); - native_backend_flush_scope(backend, options); - // Install crash handlers (signal handlers on Linux/macOS, Mach exception // handler on iOS) #if defined(SENTRY_PLATFORM_IOS) @@ -1029,6 +967,12 @@ native_backend_startup( state->ipc->shmem->platform.wer_enabled = wer_register_module(tid, options); # endif + if (!observe_scope(state)) { + SENTRY_WARN("failed to synchronize native crash scope"); + native_backend_shutdown(backend); + return 1; + } + sentry_handler_strategy_t strategy = # if defined(SENTRY_PLATFORM_LINUX) && !defined(SENTRY_PLATFORM_ANDROID) options ? sentry_options_get_handler_strategy(options) : @@ -1050,6 +994,10 @@ native_backend_startup( CloseHandle(hDaemon); } # endif + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_remove_observer(scope, state->scope_observer); + } + state->scope_observer = NULL; sentry__crash_ipc_free(state->ipc); sentry__path_free(state->run_path); sentry_free(state); @@ -1059,18 +1007,6 @@ native_backend_startup( #endif SENTRY_DEBUG("native backend started successfully"); - sentry_scope_observer_t *observer = sentry__scope_observer_new(); - if (observer) { - observer->data = state; - observer->clear = clear_scope; - observer->add_breadcrumb = native_backend_add_breadcrumb; - observer->add_attachment = add_attachment; - SENTRY_WITH_SCOPE_MUT (scope) { - if (sentry__scope_add_observer(scope, observer)) { - state->scope_observer = observer; - } - } - } return 0; } @@ -1099,16 +1035,26 @@ native_backend_shutdown(sentry_backend_t *backend) // handler on iOS) sentry__crash_handler_shutdown(); + sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN, sentry_value_new_null()); + bool daemon_stopped = false; #if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_IOS) - // Terminate daemon (Unix) if (state->daemon_pid > 0) { - kill(state->daemon_pid, SIGTERM); - // Wait for daemon to exit - pid_t wait_result; - do { - wait_result = waitpid(state->daemon_pid, NULL, 0); - } while (wait_result < 0 && errno == EINTR); + pid_t wait_result = 0; + for (int i = 0; i < 500; i++) { + wait_result = waitpid(state->daemon_pid, NULL, WNOHANG); + if (wait_result > 0 || (wait_result < 0 && errno != EINTR)) { + break; + } + usleep(10000); + } + if (wait_result == 0) { + kill(state->daemon_pid, SIGTERM); + do { + wait_result = waitpid(state->daemon_pid, NULL, 0); + } while (wait_result < 0 && errno == EINTR); + } daemon_stopped = wait_result == state->daemon_pid; } #elif defined(SENTRY_PLATFORM_WINDOWS) @@ -1117,10 +1063,13 @@ native_backend_shutdown(sentry_backend_t *backend) HANDLE hDaemon = OpenProcess( PROCESS_TERMINATE | SYNCHRONIZE, FALSE, state->daemon_pid); if (hDaemon) { - TerminateProcess(hDaemon, 0); - // Wait for daemon to exit (with timeout) daemon_stopped = WaitForSingleObject(hDaemon, 5000) == WAIT_OBJECT_0; + if (!daemon_stopped) { + TerminateProcess(hDaemon, 0); + daemon_stopped + = WaitForSingleObject(hDaemon, 5000) == WAIT_OBJECT_0; + } CloseHandle(hDaemon); } } @@ -1213,162 +1162,321 @@ native_backend_free(sentry_backend_t *backend) return; } - sentry__path_free(state->event_path); - sentry__path_free(state->breadcrumb1_path); - sentry__path_free(state->breadcrumb2_path); sentry__path_free(state->envelope_path); sentry__path_free(state->run_path); sentry_free(state); } -// Writes the attachment list to /__sentry-attachments so the -// crash daemon can locate and append them to the crash envelope. static void -native_backend_write_attachments( - const sentry_path_t *event_path, const sentry_hint_t *hint) +write_crash_attachments( + native_backend_state_t *state, const sentry_hint_t *hint) { - if (!event_path) { + if (!state || !state->run_path) { return; } - SENTRY_WITH_SCOPE (scope) { - sentry_value_t attachments = hint - ? sentry_value_incref(hint->attachments) - : sentry__scope_load_attachments(scope); - if (sentry_value_get_length(attachments) == 0 && !hint) { - sentry_value_decref(attachments); + sentry_value_t attachments = sentry__hint_resolve_attachments(hint); + size_t len = sentry_value_get_length(attachments); + for (size_t i = 0; i < len; i++) { + add_attachment(state, sentry_value_get_by_index(attachments, i)); + } + sentry_path_t *path + = sentry__path_join_str(state->run_path, "__sentry-attachments"); + if (path) { + sentry__write_attachment_manifest(path, attachments); + sentry__path_free(path); + } + sentry_value_decref(attachments); +} + +static void +write_crash_event(native_backend_state_t *state, sentry_value_t event) +{ + if (!state || !state->run_path) { + return; + } + sentry_path_t *path + = sentry__path_join_str(state->run_path, "__sentry-event"); + if (!path) { + return; + } + size_t len = 0; + char *json = sentry__value_to_json(event, &len); + if (json) { + sentry__path_write_buffer(path, json, len); + sentry_free(json); + } + sentry__path_free(path); +} + +static sentry_value_t +attachment_list(native_backend_state_t *state, const sentry_scope_t *scope) +{ + sentry_value_t attachments = sentry__scope_load_attachments(scope); + sentry_value_t attach_list = sentry_value_new_list(); + size_t len = sentry_value_get_length(attachments); + for (size_t i = 0; i < len; i++) { + sentry_value_t attachment = sentry_value_get_by_index(attachments, i); + sentry_path_t *path + = sentry__attachment_make_run_path(state->run_path, attachment); + if (!path) { continue; } - sentry_path_t *run_path = sentry__path_dir(event_path); - if (!run_path) { - sentry_value_decref(attachments); + // skip missing or partially written attachments + size_t bytes_len = 0; + if (sentry__attachment_get_bytes(attachment, &bytes_len) + && sentry__path_get_size(path) != bytes_len) { + sentry__path_free(path); continue; } - sentry_path_t *attach_list_path - = sentry__path_join_str(run_path, "__sentry-attachments"); - if (attach_list_path) { - sentry__write_attachment_manifest(attach_list_path, attachments); - sentry__path_free(attach_list_path); + sentry_value_t attach_info = sentry_value_new_object(); + sentry_value_set_by_key( + attach_info, "path", sentry_value_new_string(path->path)); + const char *filename = sentry__attachment_get_filename(attachment); + sentry_value_set_by_key( + attach_info, "filename", sentry_value_new_string(filename)); + const char *type = sentry__attachment_get_type(attachment); + if (!sentry__string_empty(type)) { + sentry_value_set_by_key( + attach_info, "attachment_type", sentry_value_new_string(type)); } - sentry__path_free(run_path); - sentry_value_decref(attachments); + const char *content_type + = sentry__attachment_get_content_type(attachment); + if (content_type) { + sentry_value_set_by_key(attach_info, "content_type", + sentry_value_new_string(content_type)); + } + sentry_value_append(attach_list, attach_info); + sentry__path_free(path); } + sentry_value_decref(attachments); + return attach_list; } -#if defined(SENTRY_PLATFORM_WINDOWS) -// Sentry's symbolicator needs `contexts.device.arch` to process PE modules. If -// the scope already carries a device context with arch (host SDKs like Unity -// provide one), leave it; otherwise synthesize a minimal one so native-only -// consumers still symbolicate. static void -ensure_device_arch(sentry_value_t event) +sync_attachments(void *data, sentry_value_t attachment) { - sentry_value_t contexts = sentry_value_get_by_key(event, "contexts"); - if (sentry_value_is_null(contexts)) { - contexts = sentry_value_new_object(); - sentry_value_set_by_key(event, "contexts", contexts); - } - sentry_value_t device = sentry_value_get_by_key(contexts, "device"); - if (sentry_value_is_null(device)) { - device = sentry_value_new_object(); - sentry_value_set_by_key( - device, "type", sentry_value_new_string("device")); - sentry_value_set_by_key(contexts, "device", device); - } - if (!sentry_value_is_null(sentry_value_get_by_key(device, "arch"))) { + (void)attachment; + native_backend_state_t *state = data; + if (sentry__atomic_fetch(&state->crashed)) { return; } -# if defined(_M_AMD64) - sentry_value_set_by_key(device, "arch", sentry_value_new_string("x86_64")); -# elif defined(_M_IX86) - sentry_value_set_by_key(device, "arch", sentry_value_new_string("x86")); -# elif defined(_M_ARM64) - sentry_value_set_by_key(device, "arch", sentry_value_new_string("arm64")); -# endif + SENTRY_WITH_SCOPE (scope) { + sentry_value_t list = attachment_list(state, scope); + sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST, list); + sentry_value_decref(list); + } } -#endif static void -native_backend_flush_scope( - sentry_backend_t *backend, const sentry_options_t *options) +observe_attachment(void *data, sentry_value_t attachment) { - native_backend_state_t *state = (native_backend_state_t *)backend->data; - if (!state || !state->event_path || sentry__atomic_fetch(&state->crashed)) { + native_backend_state_t *state = data; + if (sentry__atomic_fetch(&state->crashed)) { return; } + add_attachment(data, attachment); + sync_attachments(data, attachment); +} - native_backend_write_attachments(state->event_path, NULL); - - // Create event with current scope - sentry_value_t event = sentry_value_new_object(); - sentry__ensure_event_id(event, NULL); +static bool +send_snapshot(native_backend_state_t *state, const sentry_scope_t *scope, + sentry_value_t event) +{ + sentry_value_t snapshot = sentry_value_new_object(); + sentry_value_set_by_key(snapshot, "event", sentry_value_incref(event)); sentry_value_set_by_key( - event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); + snapshot, "attachments", attachment_list(state, scope)); + bool sent = sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, snapshot); + sentry_value_decref(snapshot); + return sent; +} - // Apply scope with contexts - SENTRY_WITH_SCOPE (scope) { - sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_NONE); +static bool +snapshot_scope(native_backend_state_t *state, const sentry_scope_t *scope) +{ + if (sentry__atomic_fetch(&state->crashed)) { + return false; } -#if defined(SENTRY_PLATFORM_WINDOWS) - ensure_device_arch(event); -#endif - - size_t json_len = 0; - char *json_str = sentry__value_to_json(event, &json_len); + sentry_value_t event = sentry_value_new_object(); + sentry__scope_apply_to_event( + scope, state->options, event, SENTRY_SCOPE_BREADCRUMBS); + bool sent = send_snapshot(state, scope, event); sentry_value_decref(event); - - if (json_str) { - sentry__path_write_buffer(state->event_path, json_str, json_len); - sentry_free(json_str); - } + return sent; } static void -native_backend_add_breadcrumb(void *data, sentry_value_t breadcrumb) +clear_scope(void *data) { - native_backend_state_t *state = (native_backend_state_t *)data; - if (!state) { + native_backend_state_t *state = data; + if (sentry__atomic_fetch(&state->crashed)) { return; } + // clear observers run before the source scope is reset + SENTRY_WITH_SCOPE (scope) { + sentry_scope_t *empty = sentry_scope_clone(scope); + if (empty) { + sentry_scope_clear(empty); + snapshot_scope(state, empty); + sentry_scope_free(empty); + } + } +} - size_t max_breadcrumbs = state->max_breadcrumbs; - if (!max_breadcrumbs) { - return; +#define VALUE_OBSERVER(Name, Type) \ + static void Name(void *data, sentry_value_t value) \ + { \ + native_backend_state_t *state = data; \ + sentry__crash_ipc_send(state->ipc, Type, value); \ } - bool first_breadcrumb = state->num_breadcrumbs % max_breadcrumbs == 0; +VALUE_OBSERVER(set_release, SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE) +VALUE_OBSERVER(set_environment, SENTRY_CRASH_IPC_MESSAGE_SET_ENVIRONMENT) +VALUE_OBSERVER(set_transaction, SENTRY_CRASH_IPC_MESSAGE_SET_TRANSACTION) +VALUE_OBSERVER(set_fingerprint, SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT) +VALUE_OBSERVER(add_breadcrumb, SENTRY_CRASH_IPC_MESSAGE_ADD_BREADCRUMB) +#undef VALUE_OBSERVER - const sentry_path_t *breadcrumb_file - = state->num_breadcrumbs % (max_breadcrumbs * 2) < max_breadcrumbs - ? state->breadcrumb1_path - : state->breadcrumb2_path; +static void +set_user(void *data, sentry_value_t user) +{ + native_backend_state_t *state = data; + sentry_value_t value = sentry__value_clone(user); + if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_OBJECT + && sentry_value_is_null(sentry_value_get_by_key(value, "id")) + && state->options->run->installation_id) { + sentry_value_set_by_key(value, "id", + sentry_value_new_string(state->options->run->installation_id)); + } + sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SET_USER, value); + sentry_value_decref(value); +} - state->num_breadcrumbs++; +static void +set_level(void *data, sentry_level_t level) +{ + native_backend_state_t *state = data; + sentry_value_t value = sentry__value_new_level(level); + sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SET_LEVEL, value); + sentry_value_decref(value); +} - if (!breadcrumb_file) { - return; +static void +send_key(void *data, uint16_t type, const char *key, sentry_value_t value) +{ + native_backend_state_t *state = data; + sentry_value_t pair = sentry_value_new_list(); + sentry_value_append(pair, sentry_value_new_string(key)); + sentry_value_append(pair, sentry_value_incref(value)); + sentry__crash_ipc_send(state->ipc, type, pair); + sentry_value_decref(pair); +} + +static void +set_tag(void *data, const char *key, sentry_value_t value) +{ + send_key(data, SENTRY_CRASH_IPC_MESSAGE_SET_TAG, key, value); +} + +static void +set_extra(void *data, const char *key, sentry_value_t value) +{ + send_key(data, SENTRY_CRASH_IPC_MESSAGE_SET_EXTRA, key, value); +} + +static void +set_context(void *data, const char *key, sentry_value_t value) +{ + if (!sentry__string_eq(key, "trace")) { + send_key(data, SENTRY_CRASH_IPC_MESSAGE_SET_CONTEXT, key, value); } +} - // Append as msgpack, matching the crashpad backend. msgpack values are - // self-delimiting, so the daemon can read the concatenated ring file back - // into a list via `sentry_value_from_msgpack`. - size_t mpack_size = 0; - char *mpack = sentry_value_to_msgpack(breadcrumb, &mpack_size); - if (!mpack) { - return; +#define REMOVE_OBSERVER(Name, Type) \ + static void Name(void *data, const char *key) \ + { \ + native_backend_state_t *state = data; \ + sentry_value_t value = sentry_value_new_string(key); \ + sentry__crash_ipc_send(state->ipc, Type, value); \ + sentry_value_decref(value); \ } - int rv = first_breadcrumb - ? sentry__path_write_buffer(breadcrumb_file, mpack, mpack_size) - : sentry__path_append_buffer(breadcrumb_file, mpack, mpack_size); +REMOVE_OBSERVER(remove_tag, SENTRY_CRASH_IPC_MESSAGE_REMOVE_TAG) +REMOVE_OBSERVER(remove_extra, SENTRY_CRASH_IPC_MESSAGE_REMOVE_EXTRA) +REMOVE_OBSERVER(remove_context_key, SENTRY_CRASH_IPC_MESSAGE_REMOVE_CONTEXT) +#undef REMOVE_OBSERVER - sentry_free(mpack); +static void +remove_context(void *data, const char *key) +{ + if (!sentry__string_eq(key, "trace")) { + remove_context_key(data, key); + } +} - if (rv != 0) { - SENTRY_WARN("failed to write breadcrumb"); +static void +set_trace(void *data, sentry_value_t trace) +{ + if (sentry_value_is_null(trace)) { + remove_context_key(data, "trace"); + } else { + send_key(data, SENTRY_CRASH_IPC_MESSAGE_SET_CONTEXT, "trace", trace); } } +static bool +init_scope_observer(void *data, const sentry_scope_t *scope) +{ + native_backend_state_t *state = data; + sentry_value_t attachments = sentry__scope_load_attachments(scope); + for (size_t i = 0; i < sentry_value_get_length(attachments); i++) { + add_attachment(state, sentry_value_get_by_index(attachments, i)); + } + sentry_value_decref(attachments); + return snapshot_scope(state, scope); +} + +static bool +observe_scope(native_backend_state_t *state) +{ + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + if (!observer) { + return false; + } + observer->data = state; + observer->clear = clear_scope; + observer->set_release = set_release; + observer->set_environment = set_environment; + observer->set_transaction = set_transaction; + observer->set_fingerprint = set_fingerprint; + observer->set_level = set_level; + observer->set_user = set_user; + observer->set_tag = set_tag; + observer->remove_tag = remove_tag; + observer->set_extra = set_extra; + observer->remove_extra = remove_extra; + observer->set_context = set_context; + observer->remove_context = remove_context; + observer->set_trace = set_trace; + observer->add_breadcrumb = add_breadcrumb; + observer->add_attachment = observe_attachment; + observer->remove_attachment = sync_attachments; + bool sent = false; + SENTRY_WITH_SCOPE_MUT (scope) { + if (sentry__scope_add_observer_with_init( + scope, observer, init_scope_observer)) { + state->scope_observer = observer; + sent = true; + } + } + return sent; +} + /** * Handle exception - called from signal handler via sentry_handle_exception * This processes the event with on_crash/before_send hooks and ends the session @@ -1379,6 +1487,9 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) native_backend_state_t *state = (native_backend_state_t *)backend->data; if (state) { sentry__atomic_store(&state->crashed, 1); + if (state->ipc) { + sentry__atomic_store(&state->ipc->stopped, 1); + } } SENTRY_WITH_OPTIONS (options) { @@ -1435,26 +1546,12 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) } if (should_handle) { - bool modified = sentry__hint_is_modified(&hint); - if (modified) { - size_t len = sentry_value_get_length(hint.attachments); - for (size_t i = 0; i < len; i++) { - add_attachment(state, - sentry_value_get_by_index(hint.attachments, i)); - } - } - // Write modified hint attachments or reload the current scope - native_backend_write_attachments( - state ? state->event_path : NULL, modified ? &hint : NULL); - // Apply scope to the event. The daemon assembles breadcrumbs - // from the ring files + write_crash_attachments(state, &hint); SENTRY_WITH_SCOPE (scope) { sentry__scope_apply_to_event( - scope, options, event, SENTRY_SCOPE_NONE); + scope, options, event, SENTRY_SCOPE_BREADCRUMBS); } -#if defined(SENTRY_PLATFORM_WINDOWS) - ensure_device_arch(event); -#endif + write_crash_event(state, event); #ifndef SENTRY_SCREENSHOT_NONE // The screenshot is captured by the daemon out-of-process, so @@ -1476,24 +1573,6 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) } #endif - // Write event as JSON file - // Daemon will read this and create envelope with minidump - if (state && state->event_path) { - size_t event_json_len = 0; - char *event_json - = sentry__value_to_json(event, &event_json_len); - if (event_json) { - int rv = sentry__path_write_buffer( - state->event_path, event_json, event_json_len); - sentry_free(event_json); - if (rv == 0) { - SENTRY_DEBUG("Wrote crash event JSON for daemon"); - } else { - SENTRY_WARN("Failed to write event JSON"); - } - } - } - sentry_value_decref(event); // End session with crashed status and write session envelope to @@ -1537,7 +1616,7 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) sentry_value_decref(transaction); } if (!should_handle && ctx) { - ctx->event_path[0] = '\0'; + ctx->event_discarded = true; } sentry__hint_deinit(&hint); } @@ -1570,7 +1649,6 @@ sentry__backend_new(void) backend->shutdown_func = native_backend_shutdown; backend->free_func = native_backend_free; backend->except_func = native_backend_except; - backend->flush_scope_func = native_backend_flush_scope; backend->user_consent_changed_func = native_backend_user_consent_changed; backend->process_old_run_func = native_backend_process_old_run; backend->reinstall_func = native_backend_reinstall; diff --git a/src/integrations/sentry_integration_wer.c b/src/integrations/sentry_integration_wer.c index e7156fb9fe..11be4031f8 100644 --- a/src/integrations/sentry_integration_wer.c +++ b/src/integrations/sentry_integration_wer.c @@ -62,8 +62,9 @@ sentry_integration_wer_free(void *data) } static void -wer_set_tag(void *data, const char *key, const char *value) +wer_set_tag(void *data, const char *key, sentry_value_t tag) { + const char *value = sentry_value_as_string(tag); sentry_integration_wer_data_t *wer_data = (sentry_integration_wer_data_t *)data; if (!wer_data->WerRegisterCustomMetadata) { @@ -135,7 +136,7 @@ static int wer_sync_tag(const char *key, sentry_value_t value, void *data) { if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_STRING) { - wer_set_tag(data, key, sentry_value_as_string(value)); + wer_set_tag(data, key, value); } return 0; } diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 6d508fb620..c305b1372a 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -716,6 +716,14 @@ sentry__scope_observer_new(void) bool sentry__scope_add_observer( sentry_scope_t *scope, sentry_scope_observer_t *observer) +{ + return sentry__scope_add_observer_with_init(scope, observer, NULL); +} + +bool +sentry__scope_add_observer_with_init(sentry_scope_t *scope, + sentry_scope_observer_t *observer, + bool (*init)(void *data, const sentry_scope_t *scope)) { if (!observer) { return false; @@ -738,6 +746,11 @@ sentry__scope_add_observer( new_array[scope->num_observers] = observer; scope->observers = new_array; scope->num_observers = new_count; + if (init && !init(observer->data, scope)) { + sentry__scope_remove_observer(scope, observer); + sentry__mutex_unlock(&scope->observers_lock); + return false; + } sentry__mutex_unlock(&scope->observers_lock); return true; } @@ -1117,6 +1130,46 @@ sentry__scope_load_propagation_context(const sentry_scope_t *scope) return propagation_context; } +static void +notify_trace(sentry_scope_t *scope) +{ + lock_scope_notify(scope); + bool observed = false; + for (size_t i = 0; i < scope->num_observers; i++) { + if (scope->observers[i] && scope->observers[i]->set_trace) { + observed = true; + break; + } + } + if (!observed) { + unlock_scope_notify(scope); + return; + } + sentry_value_t span = sentry__scope_load_span_or_transaction(scope); + sentry_value_t trace = sentry__value_get_trace_context(span); + if (!sentry_value_is_null(trace)) { + sentry_value_t data = sentry_value_get_by_key(span, "data"); + if (!sentry_value_is_null(data)) { + sentry_value_set_by_key(trace, "data", sentry_value_incref(data)); + } + } else { + sentry_value_t contexts = sentry__scope_load_contexts(scope); + trace = sentry__value_clone(sentry_value_get_by_key(contexts, "trace")); + sentry_value_decref(contexts); + sentry_value_t propagation = sentry__scope_load_trace_context(scope); + if (sentry_value_is_null(trace)) { + trace = propagation; + } else { + sentry__value_merge_objects(trace, propagation); + sentry_value_decref(propagation); + } + } + SENTRY_SCOPE_NOTIFY(scope, set_trace, trace); + sentry_value_decref(trace); + sentry_value_decref(span); + unlock_scope_notify(scope); +} + void sentry__scope_set_propagation_context( sentry_scope_t *scope, const char *key, sentry_value_t value) @@ -1124,6 +1177,7 @@ sentry__scope_set_propagation_context( SENTRY_SCOPE_WRITE_LOCK (scope->data) { sentry_value_set_by_key(scope->data->propagation_context, key, value); } + notify_trace(scope); sentry__scope_flush(scope); } @@ -1133,6 +1187,7 @@ sentry__scope_regenerate_propagation_context(sentry_scope_t *scope) SENTRY_SCOPE_WRITE_LOCK (scope->data) { generate_propagation_context(scope->data->propagation_context); } + notify_trace(scope); } sentry_value_t @@ -1158,6 +1213,7 @@ sentry__scope_set_trace_context( sentry_value_get_by_key(scope->data->propagation_context, "trace"), key, value); } + notify_trace(scope); } bool @@ -1672,17 +1728,19 @@ void sentry_scope_set_tag(sentry_scope_t *scope, const char *key, const char *value) { sentry_value_t tag_value = sentry_value_new_string(value); + sentry_value_t stored_value = sentry_value_incref(tag_value); bool did_set = false; lock_scope_notify(scope); SENTRY_SCOPE_WRITE_LOCK (scope->data) { - did_set - = sentry_value_set_by_key(scope->data->tags, key, tag_value) == 0; + did_set = sentry_value_set_by_key(scope->data->tags, key, stored_value) + == 0; } if (did_set) { - SENTRY_SCOPE_NOTIFY(scope, set_tag, key, value); + SENTRY_SCOPE_NOTIFY(scope, set_tag, key, tag_value); } unlock_scope_notify(scope); sentry__scope_flush(scope); + sentry_value_decref(tag_value); } void @@ -1705,8 +1763,7 @@ sentry_scope_set_tag_n(sentry_scope_t *scope, const char *key, size_t key_len, == 0; } if (did_set) { - SENTRY_SCOPE_NOTIFY( - scope, set_tag, notify_key, sentry_value_as_string(tag_value)); + SENTRY_SCOPE_NOTIFY(scope, set_tag, notify_key, tag_value); } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -1721,8 +1778,9 @@ set_tag_value(const char *key, sentry_value_t value, void *userdata) SENTRY_WARNF("set_tags: ignoring non-string value for tag `%s`", key); return 0; } - sentry_scope_set_tag( - (sentry_scope_t *)userdata, key, sentry_value_as_string(value)); + sentry_scope_set_tag_n((sentry_scope_t *)userdata, key, + sentry__guarded_strlen(key), sentry_value_as_string(value), + sentry_value_get_length(value)); return 0; } @@ -1975,6 +2033,9 @@ sentry_scope_set_context( } if (did_set) { SENTRY_SCOPE_NOTIFY(scope, set_context, key, value); + if (sentry__string_eq(key, "trace")) { + notify_trace(scope); + } } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -2001,6 +2062,9 @@ sentry_scope_set_context_n(sentry_scope_t *scope, const char *key, } if (did_set) { SENTRY_SCOPE_NOTIFY(scope, set_context, notify_key, value); + if (key_len == 5 && memcmp(key, "trace", 5) == 0) { + notify_trace(scope); + } } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -2018,6 +2082,9 @@ sentry_scope_remove_context(sentry_scope_t *scope, const char *key) } if (removed) { SENTRY_SCOPE_NOTIFY(scope, remove_context, key); + if (sentry__string_eq(key, "trace")) { + notify_trace(scope); + } } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -2035,6 +2102,9 @@ sentry_scope_remove_context_n( } if (removed_key) { SENTRY_SCOPE_NOTIFY(scope, remove_context, removed_key); + if (key_len == 5 && memcmp(removed_key, "trace", 5) == 0) { + notify_trace(scope); + } } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -2074,6 +2144,9 @@ sentry_scope_update_context_n(sentry_scope_t *scope, const char *key, } if (did_set) { SENTRY_SCOPE_NOTIFY(scope, set_context, notify_key, value); + if (key_len == 5 && memcmp(key, "trace", 5) == 0) { + notify_trace(scope); + } } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -2436,6 +2509,7 @@ sentry__scope_set_transaction_object( sentry__transaction_decref(scope->data->transaction_object); scope->data->transaction_object = transaction; } + notify_trace(scope); sentry__scope_flush(scope); } @@ -2452,6 +2526,7 @@ sentry__scope_remove_transaction_object( } if (removed) { sentry__transaction_decref(transaction); + notify_trace(scope); } if (removed) { sentry__scope_flush(scope); @@ -2475,6 +2550,9 @@ sentry__scope_remove_transaction_value( } } sentry__transaction_decref(transaction_object); + if (transaction_object) { + notify_trace(scope); + } return transaction_object != NULL; } @@ -2491,6 +2569,7 @@ sentry__scope_restore_transaction_object( } } if (restored) { + notify_trace(scope); sentry__scope_flush(scope); } return restored; @@ -2541,6 +2620,7 @@ sentry__scope_set_span(sentry_scope_t *scope, sentry_span_t *span) sentry__span_decref(scope->data->span); scope->data->span = span; } + notify_trace(scope); sentry__scope_flush(scope); } @@ -2556,6 +2636,7 @@ sentry__scope_remove_span(sentry_scope_t *scope, sentry_span_t *span) } if (removed) { sentry__span_decref(span); + notify_trace(scope); } if (removed) { sentry__scope_flush(scope); @@ -2578,6 +2659,7 @@ sentry__scope_remove_span_value(sentry_scope_t *scope, sentry_value_t span) } sentry__span_decref(scope_span); if (scope_span != NULL) { + notify_trace(scope); sentry__scope_flush(scope); } return scope_span != NULL; @@ -2594,6 +2676,7 @@ sentry__scope_restore_span(sentry_scope_t *scope, sentry_span_t *span) } } if (restored) { + notify_trace(scope); sentry__scope_flush(scope); } return restored; diff --git a/src/sentry_scope.h b/src/sentry_scope.h index f56e9f3e07..b3e2777324 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -37,7 +37,7 @@ typedef struct sentry_scope_observer_s { void (*add_breadcrumb)(void *data, sentry_value_t breadcrumb); - void (*set_tag)(void *data, const char *key, const char *value); + void (*set_tag)(void *data, const char *key, sentry_value_t value); void (*remove_tag)(void *data, const char *key); void (*set_extra)(void *data, const char *key, sentry_value_t value); @@ -48,6 +48,8 @@ typedef struct sentry_scope_observer_s { void (*add_attachment)(void *data, sentry_value_t attachment); void (*remove_attachment)(void *data, sentry_value_t attachment); + + void (*set_trace)(void *data, sentry_value_t trace); } sentry_scope_observer_t; typedef struct sentry_scope_data_s sentry_scope_data_t; @@ -249,6 +251,18 @@ sentry_scope_observer_t *sentry__scope_observer_new(void); bool sentry__scope_add_observer( sentry_scope_t *scope, sentry_scope_observer_t *observer); +/** + * Register and initialize a scope observer. + * + * Calls `init` with the observer's data and the scope after registration, + * while scope notifications are locked. The callback must only read the scope. + * Returning false removes the observer. Takes ownership of `observer`, + * freeing it if registration or initialization fails. + */ +bool sentry__scope_add_observer_with_init(sentry_scope_t *scope, + sentry_scope_observer_t *observer, + bool (*init)(void *data, const sentry_scope_t *scope)); + /** * Remove a scope observer. * diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 1f529fb820..e77457c384 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -5,14 +5,12 @@ multi-thread capture, and FPU/SIMD register capture on all platforms. """ -import json import os import subprocess import sys import time import struct -import msgpack import pytest from . import ( @@ -384,21 +382,17 @@ def test_native_capture_minidump_generated(cmake, httpserver): assert not list(db_dir.glob("*.run*.lock")) -# Both daemon envelope writers merge the breadcrumb ring files: the native -# stacktrace writer builds the event from scratch, while the minidump-only -# writer re-parses the parent's event JSON, merges, and re-serializes. Exercise -# both so the minidump path's extra parse/serialize roundtrip is covered too. +# Both daemon envelope writers use the scope received over IPC. +# Exercise breadcrumbs in both native-stacktrace and minidump-only events. BREADCRUMB_CRASH_MODES = ["native", "minidump"] @pytest.mark.parametrize("crash_mode", BREADCRUMB_CRASH_MODES) def test_native_breadcrumbs(cmake, httpserver, crash_mode): - """Test that breadcrumbs survive the daemon's ring-file merge. + """Test that breadcrumbs survive the daemon's IPC scope updates. - The crashing process appends breadcrumbs as msgpack to the ring files; the - daemon reads, merges, and attaches them to the event. Asserting on the - default `debug crumb` verifies that whole roundtrip, not just that an event - arrived. + Asserting on the default `debug crumb` verifies the whole roundtrip, not + just that an event arrived. """ tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "native"}) @@ -423,7 +417,7 @@ def test_native_breadcrumbs(cmake, httpserver, crash_mode): @pytest.mark.parametrize("crash_mode", BREADCRUMB_CRASH_MODES) def test_native_overflow_breadcrumbs(cmake, httpserver, crash_mode): - """Test that the daemon caps merged breadcrumbs at max_breadcrumbs. + """Test that the daemon caps staged breadcrumbs at max_breadcrumbs. The example adds 3 default crumbs plus 101 numbered crumbs ("0".."100"). With the default max_breadcrumbs (100), the daemon keeps the newest 100, @@ -457,62 +451,61 @@ def test_native_overflow_breadcrumbs(cmake, httpserver, crash_mode): assert any(b.get("message") == "100" for b in breadcrumbs) -def test_native_attachment_manifest_is_current(cmake, httpserver): - """The attachment manifest must reflect the live attachment state (#1933). - - The daemon builds hard-crash envelopes from `/__sentry-attachments`, - which the backend only rewrites on a scope flush. `sentry_add_attachment` - adds the fully configured attachment before that flush, so the manifest - must contain all its metadata immediately. - - Asserting on the manifest rather than on a crash envelope keeps this - deterministic: the in-process crash handler happens to flush the scope (via - `sentry__trace_finish`), which repairs the manifest before the daemon reads - it, but out-of-process paths such as WER never run that handler. - """ - tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "native"}) - - cmd = run_command(str(tmp_path / "sentry_test_integration")) - child = subprocess.Popen( - [*cmd, "log", "attach-custom-filename", "sleep"], - cwd=tmp_path, - env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), +@pytest.mark.parametrize("mode", [0, 1, 2]) +@pytest.mark.parametrize("scoped_trace", [False, True]) +def test_native_scope_updates_without_snapshot(cmake, httpserver, mode, scoped_trace): + tmp_path = cmake(["sentry_test_unit", "sentry-crash"], {"SENTRY_BACKEND": "native"}) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + env = dict( + os.environ, + SENTRY_DSN=make_dsn(httpserver), + SENTRY_TEST_NATIVE_SCOPE_CRASH=str(mode), ) - db_dir = tmp_path / ".sentry-native" - - last_manifest = None - - def manifest_is_current(): - # Validate the complete expected state against a single snapshot - # instead of waiting on one field and re-reading the file. - nonlocal last_manifest - paths = list(db_dir.glob("*.run/__sentry-attachments")) - if not paths: - return False - try: - unpacker = msgpack.Unpacker(raw=False) - unpacker.feed(paths[0].read_bytes()) - manifest = list(unpacker) - except (OSError, msgpack.UnpackException): - # rewritten in place, so a read can catch a partial file - return False - last_manifest = manifest - return ( - len(manifest) == 1 - and manifest[0].get("filename") == "custom-name.log" - and manifest[0].get("content_type") == "application/zstd" - and manifest[0].get("path", "").endswith("CMakeCache.txt") + if scoped_trace: + env["SENTRY_TEST_NATIVE_SCOPE_SPAN"] = "1" + with httpserver.wait(timeout=15) as waiting: + run_crash( + tmp_path, + "sentry_test_unit", + ["--no-exec", "native_scope_updates"], + env=env, ) - - try: - up_to_date = wait_for(manifest_is_current) - finally: - child.terminate() - child.wait() - - assert ( - up_to_date - ), f"attachment manifest does not reflect the live attachment state: {last_manifest}" + assert waiting.result + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + event = envelope.get_event() + assert event["release"] == "ipc-release" + assert event["environment"] == "ipc-environment" + assert event["transaction"] == "ipc-transaction" + assert event["level"] == "fatal" + assert event["fingerprint"] == ["ipc", "latest"] + assert event["user"]["username"] == "ipc-user" + assert event["user"]["id"] + assert event["tags"] == {"tag": "latest", "binary-tag": "v\0x"} + assert event["extra"]["large"] == "x" * (1024 * 1024) + assert set(event["extra"]) == {"large", "expected-trace"} + contexts = {"trace", "ipc", "os"} + if sys.platform == "win32" or is_wine: + contexts.add("device") + if is_wine: + contexts.add("wine") + assert set(event["contexts"]) == contexts + assert event["contexts"]["ipc"] == {"status": "latest"} + assert event["contexts"]["trace"] == event["extra"]["expected-trace"] + assert [crumb["index"] for crumb in event["breadcrumbs"]] == [2, 3, 4] + attachments = { + item.headers.get("filename"): item.payload.bytes + for item in envelope + if item.headers.get("type") == "attachment" + and item.headers.get("attachment_type") != "event.minidump" + } + assert attachments == {"latest.txt": b"new"} + for name in ( + "__sentry-event", + "__sentry-breadcrumb1", + "__sentry-breadcrumb2", + "__sentry-attachments", + ): + assert not list((tmp_path / ".sentry-native").glob(f"*.run/{name}")) def test_native_byte_attachment_is_confined(cmake, unreachable_dsn): @@ -1753,8 +1746,7 @@ def daemon_log(): assert "Parent process exited without crash" not in daemon_log() assert "Crash notification received, processing" not in daemon_log() assert not list(db_dir.glob("*.run/*.dmp")) - snapshot = json.loads(next(db_dir.glob("*.run/__sentry-event")).read_text()) - assert "exception" not in snapshot + assert not list(db_dir.glob("*.run/__sentry-event")) finally: if child.poll() is None: child.kill() @@ -1797,35 +1789,22 @@ def test_native_acquired_scope(cmake, httpserver, clear): def test_native_scope_snapshot(cmake, httpserver): tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "native"}) - cmd = run_command(str(tmp_path / "sentry_test_integration")) - child = subprocess.Popen( - [*cmd, "initial-scope", "no-setup", "sleep"], - cwd=tmp_path, - env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), - ) - db_dir = tmp_path / ".sentry-native" - snapshot = None - - def scope_was_written(): - nonlocal snapshot - paths = list(db_dir.glob("*.run/__sentry-event")) - if not paths: - return False - try: - snapshot = json.loads(paths[0].read_text()) - except (OSError, json.JSONDecodeError): - return False - return snapshot.get("tags", {}).get("test.initial-tag") == "initial-value" + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") - try: - assert wait_for(scope_was_written) - assert snapshot["event_id"] - assert snapshot["level"] == "fatal" - assert "exception" not in snapshot - finally: - child.terminate() - child.wait() - assert wait_for(lambda: not list(db_dir.glob("*.run.daemon.lock"))) + with httpserver.wait(timeout=10) as waiting: + run_crash( + tmp_path, + "sentry_test_integration", + ["initial-scope", "no-setup", "crash"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + assert waiting.result + + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + event = envelope.get_event() + assert event["tags"]["test.initial-tag"] == "initial-value" + assert event["event_id"] + assert event["level"] == "fatal" @pytest.mark.parametrize("crash_mode", ["native", "native-with-minidump"]) @@ -1892,3 +1871,55 @@ def test_native_symbolication(cmake, httpserver, crash_mode, symbolize): assert any( "function" in frame for frame in exc["stacktrace"]["frames"] ), "missing symbolicated function names" + + +@pytest.mark.parametrize("clear_hint", [False, True]) +def test_native_scope_updates_in_crash_callback(cmake, httpserver, clear_hint): + tmp_path = cmake(["sentry_test_unit", "sentry-crash"], {"SENTRY_BACKEND": "native"}) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + env = dict( + os.environ, + SENTRY_DSN=make_dsn(httpserver), + SENTRY_TEST_NATIVE_SCOPE_CRASH="1", + SENTRY_TEST_NATIVE_SCOPE_CALLBACK="1", + ) + if clear_hint: + env["SENTRY_TEST_NATIVE_SCOPE_HINT_CLEAR"] = "1" + with httpserver.wait(timeout=15) as waiting: + run_crash( + tmp_path, + "sentry_test_unit", + ["--no-exec", "native_scope_updates"], + env=env, + ) + assert waiting.result + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + event = envelope.get_event() + assert event["callback"] is True + assert event["tags"]["tag"] == "callback" + attachments = [ + item for item in envelope if item.headers.get("type") == "attachment" + ] + if clear_hint: + assert attachments == [] + else: + assert len(attachments) == 1 + assert attachments[0].headers["filename"] == "callback.txt" + assert attachments[0].payload.bytes == b"callback" + + +@pytest.mark.skipif( + sys.platform != "linux" or is_wine, + reason="Test uses Linux daemon startup", +) +def test_native_shutdown_during_crash(cmake): + tmp_path = cmake( + ["sentry_test_unit", "sentry-crash"], + {"SENTRY_BACKEND": "native", "SENTRY_TRANSPORT": "none"}, + ) + run( + tmp_path, + "sentry_test_unit", + ["--no-exec", "crash_daemon_shutdown_during_crash"], + env=dict(os.environ, SENTRY_TEST_NATIVE_DAEMON=str(tmp_path / "sentry-crash")), + ) diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index f91860946b..d74933021e 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -5,11 +5,16 @@ * and low-level crash handling functionality. */ +#include "sentry_backend.h" +#include "sentry_core.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" #include "sentry_path.h" +#include "sentry_scope.h" #include "sentry_testsupport.h" +#include "sentry_tracing.h" +#include #include #ifdef SENTRY_BACKEND_NATIVE @@ -42,6 +47,7 @@ count_sent_envelopes(sentry_envelope_t *envelope, void *state) # include # include # include +# include # include #endif @@ -924,7 +930,7 @@ SENTRY_TEST(crash_ipc_message_oversized) SKIP_TEST(); #else char oversized_len[4] = { 0 }; - uint32_t len = SENTRY_CRASH_IPC_MESSAGE_MAX_LEN; + uint32_t len = SENTRY_CRASH_IPC_MESSAGE_MAX_LEN + 1; oversized_len[0] = (char)(len & 0xffu); oversized_len[1] = (char)((len >> 8) & 0xffu); oversized_len[2] = (char)((len >> 16) & 0xffu); @@ -933,11 +939,7 @@ SENTRY_TEST(crash_ipc_message_oversized) sentry_crash_ipc_message_t message; sentry_crash_ipc_message_result_t result = sentry__crash_ipc_message_decode( oversized_len, sizeof(oversized_len), &message); -# if SIZE_MAX <= UINT32_MAX TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_OVERSIZED); -# else - TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_PARTIAL); -# endif char payload = 0; char *buf = NULL; @@ -983,3 +985,549 @@ SENTRY_TEST(crash_ipc_message_large_roundtrip) sentry_free(payload); #endif } + +#ifdef SENTRY_BACKEND_NATIVE +static bool +apply_scope_message( + sentry_crash_scope_t *scope, uint16_t type, sentry_value_t value) +{ + size_t len = 0; + char *payload = sentry_value_to_msgpack(value, &len); + sentry_crash_ipc_message_t message + = { type, 0, scope->sequence + 1, payload, len }; + bool applied = sentry__crash_scope_apply(scope, &message); + sentry_free(payload); + sentry_value_decref(value); + return applied; +} + +static sentry_value_t +scope_snapshot(void) +{ + sentry_value_t snapshot = sentry_value_new_object(); + sentry_value_set_by_key(snapshot, "event", sentry_value_new_object()); + sentry_value_set_by_key(snapshot, "attachments", sentry_value_new_list()); + return snapshot; +} + +static sentry_value_t +scope_pair(const char *key, sentry_value_t value) +{ + sentry_value_t pair = sentry_value_new_list(); + sentry_value_append(pair, sentry_value_new_string(key)); + sentry_value_append(pair, value); + return pair; +} +#endif + +SENTRY_TEST(crash_scope_updates) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_scope_t scope; + TEST_ASSERT(sentry__crash_scope_init(&scope, 2)); + TEST_CHECK(!apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_object())); + TEST_ASSERT(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, scope_snapshot())); + struct { + uint16_t set; + uint16_t remove; + const char *field; + } keyed[] = { + { SENTRY_CRASH_IPC_MESSAGE_SET_TAG, SENTRY_CRASH_IPC_MESSAGE_REMOVE_TAG, + "tags" }, + { SENTRY_CRASH_IPC_MESSAGE_SET_EXTRA, + SENTRY_CRASH_IPC_MESSAGE_REMOVE_EXTRA, "extra" }, + { SENTRY_CRASH_IPC_MESSAGE_SET_CONTEXT, + SENTRY_CRASH_IPC_MESSAGE_REMOVE_CONTEXT, "contexts" }, + }; + for (size_t i = 0; i < sizeof(keyed) / sizeof(keyed[0]); i++) { + TEST_CHECK(apply_scope_message(&scope, keyed[i].set, + scope_pair("key", sentry_value_new_string("first")))); + TEST_CHECK(apply_scope_message( + &scope, keyed[i].remove, sentry_value_new_string("key"))); + TEST_CHECK(sentry_value_is_null(sentry_value_get_by_key( + sentry_value_get_by_key(scope.event, keyed[i].field), "key"))); + TEST_CHECK(apply_scope_message(&scope, keyed[i].set, + scope_pair("key", sentry_value_new_string("last")))); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(scope.event, keyed[i].field), "key")), + "last"); + } + struct { + uint16_t type; + const char *field; + } fields[] = { + { SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE, "release" }, + { SENTRY_CRASH_IPC_MESSAGE_SET_ENVIRONMENT, "environment" }, + { SENTRY_CRASH_IPC_MESSAGE_SET_TRANSACTION, "transaction" }, + { SENTRY_CRASH_IPC_MESSAGE_SET_LEVEL, "level" }, + }; + for (size_t i = 0; i < sizeof(fields) / sizeof(fields[0]); i++) { + TEST_CHECK(apply_scope_message( + &scope, fields[i].type, sentry_value_new_string_n("a\0b", 3))); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(sentry_value_get_by_key( + scope.event, fields[i].field)), + 3); + TEST_CHECK(apply_scope_message( + &scope, fields[i].type, sentry_value_new_null())); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope.event, fields[i].field))); + } + TEST_CHECK(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_object())); + TEST_CHECK(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_null())); + TEST_CHECK(apply_scope_message(&scope, + SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT, sentry_value_new_list())); + TEST_CHECK(apply_scope_message(&scope, + SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT, sentry_value_new_null())); + for (int i = 0; i < 3; i++) { + sentry_value_t crumb = sentry_value_new_object(); + sentry_value_set_by_key(crumb, "index", sentry_value_new_int32(i)); + TEST_CHECK(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_ADD_BREADCRUMB, crumb)); + } + sentry_value_t event = sentry__crash_scope_event(&scope); + sentry_value_t crumbs = sentry_value_get_by_key(event, "breadcrumbs"); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(crumbs), 2); + TEST_CHECK_INT_EQUAL(sentry_value_as_int32(sentry_value_get_by_key( + sentry_value_get_by_index(crumbs, 0), "index")), + 1); + sentry_value_decref(event); + sentry_value_t attachments = sentry_value_new_list(); + sentry_value_append(attachments, sentry_value_new_object()); + TEST_CHECK(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST, attachments)); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(scope.attachments), 1); + TEST_CHECK(apply_scope_message(&scope, + SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST, sentry_value_new_list())); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(scope.attachments), 0); + TEST_CHECK(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, scope_snapshot())); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(scope.event), 0); + sentry_crash_ipc_message_t crash + = { SENTRY_CRASH_IPC_MESSAGE_CRASH, 0, scope.sequence + 1, NULL, 0 }; + TEST_CHECK(sentry__crash_scope_apply(&scope, &crash)); + TEST_CHECK(!apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_object())); + sentry__crash_scope_free(&scope); +#endif +} + +SENTRY_TEST(crash_scope_invalid_updates) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_scope_t scope; + TEST_ASSERT(sentry__crash_scope_init(&scope, 0)); + TEST_ASSERT(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, scope_snapshot())); + TEST_CHECK(!apply_scope_message(&scope, SENTRY_CRASH_IPC_MESSAGE_SET_TAG, + sentry_value_new_string("invalid"))); + TEST_CHECK(!apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_int32(1))); + TEST_CHECK(!apply_scope_message(&scope, + SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST, + sentry_value_new_object())); + sentry_crash_ipc_message_t message + = { SENTRY_CRASH_IPC_MESSAGE_SET_USER, 0, 2, "\x81", 1 }; + TEST_CHECK(!sentry__crash_scope_apply(&scope, &message)); + message.payload = "\xc0\xc0"; + message.payload_len = 2; + TEST_CHECK(!sentry__crash_scope_apply(&scope, &message)); + message.payload = "\xc0"; + message.payload_len = 1; + message.sequence = 3; + TEST_CHECK(!sentry__crash_scope_apply(&scope, &message)); + message.sequence = 2; + message.flags = 1; + TEST_CHECK(!sentry__crash_scope_apply(&scope, &message)); + TEST_CHECK_INT_EQUAL(scope.sequence, 1); + TEST_CHECK(apply_scope_message(&scope, + SENTRY_CRASH_IPC_MESSAGE_ADD_BREADCRUMB, sentry_value_new_object())); + sentry_value_t event = sentry__crash_scope_event(&scope); + TEST_CHECK_INT_EQUAL( + sentry_value_get_length(sentry_value_get_by_key(event, "breadcrumbs")), + 0); + sentry_value_decref(event); + sentry__crash_scope_free(&scope); +#endif +} + +#ifdef SENTRY_BACKEND_NATIVE +static sentry_value_t +native_scope_on_crash(const sentry_ucontext_t *UNUSED(uctx), + sentry_value_t event, sentry_hint_t *hint, void *UNUSED(data)) +{ + sentry_set_tag("tag", "callback"); + if (getenv("SENTRY_TEST_NATIVE_SCOPE_HINT_CLEAR")) { + sentry_hint_clear_attachments(hint); + } else { + sentry_clear_attachments(); + sentry_attach_bytes("callback", 8, "callback.txt"); + } + sentry_value_set_by_key(event, "callback", sentry_value_new_bool(true)); + return event; +} +#endif + +SENTRY_TEST(native_scope_updates) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + const char *crash_mode = getenv("SENTRY_TEST_NATIVE_SCOPE_CRASH"); + bool callback = getenv("SENTRY_TEST_NATIVE_SCOPE_CALLBACK") != NULL; + SENTRY_TEST_OPTIONS_NEW(options); + if (callback) { + sentry_options_set_on_crash(options, native_scope_on_crash, NULL); + } + sentry_options_set_auto_session_tracking(options, false); + sentry_options_set_max_breadcrumbs(options, 3); + sentry_options_set_traces_sample_rate(options, 1); + sentry_options_set_debug(options, true); + if (crash_mode) { + sentry_options_set_dsn(options, getenv("SENTRY_DSN")); + sentry_options_set_crash_reporting_mode(options, atoi(crash_mode)); + } + TEST_ASSERT(sentry_init(options) == 0); + sentry_set_tag("cleared", "old"); + sentry_attach_bytes("old", 3, "cleared.txt"); + SENTRY_WITH_SCOPE_MUT (scope) { + sentry_scope_clear(scope); + } + for (int i = 0; i < 1000; i++) { + sentry_set_tag("tag", "old"); + sentry_remove_tag("tag"); + } + sentry_set_tag("tag", "latest"); + sentry_set_tag_n("binary-tag-tail", 10, "v\0x", 3); + sentry_set_tag_n("gone-tag-tail", 8, "old", 3); + sentry_remove_tag_n("gone-tag-tail", 8); + sentry_set_extra_n("gone-extra-tail", 10, sentry_value_new_int32(1)); + sentry_remove_extra_n("gone-extra-tail", 10); + sentry_set_context_n("gone-context-tail", 12, sentry_value_new_object()); + sentry_remove_context_n("gone-context-tail", 12); + sentry_set_release("ipc-release"); + sentry_set_environment("ipc-environment"); + sentry_set_transaction("ipc-transaction"); + sentry_set_level(SENTRY_LEVEL_WARNING); + sentry_set_fingerprint("ipc", "latest", NULL); + sentry_value_t user = sentry_value_new_object(); + sentry_value_set_by_key( + user, "username", sentry_value_new_string("ipc-user")); + sentry_set_user(user); + sentry_set_extra("removed", sentry_value_new_int32(1)); + sentry_remove_extra("removed"); + size_t size = 1024 * 1024; + char *large = sentry_malloc(size); + TEST_ASSERT(large != NULL); + memset(large, 'x', size); + sentry_set_extra("large", sentry_value_new_string_n(large, size)); + sentry_free(large); + sentry_set_context("removed", sentry_value_new_object()); + sentry_remove_context("removed"); + sentry_value_t context = sentry_value_new_object(); + sentry_value_set_by_key( + context, "status", sentry_value_new_string("latest")); + sentry_set_context("ipc", context); + sentry_transaction_t *tx = sentry_transaction_start( + sentry_transaction_context_new("bound", "work"), + sentry_value_new_null()); + sentry_set_transaction_object(tx); + sentry_span_t *span = sentry_transaction_start_child(tx, "child", "work"); + sentry_set_span(span); + bool scoped_trace = getenv("SENTRY_TEST_NATIVE_SCOPE_SPAN") != NULL; + if (!scoped_trace) { + sentry_set_span(NULL); + } + sentry_set_context("trace", sentry_value_new_object()); + sentry_value_t trace = sentry_value_new_null(); + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_regenerate_propagation_context(scope); + if (scoped_trace) { + sentry_value_t active + = sentry__scope_load_span_or_transaction(scope); + trace = sentry__value_get_trace_context(active); + sentry_value_decref(active); + } else { + trace = sentry__scope_load_trace_context(scope); + } + } + sentry_set_extra("expected-trace", trace); + for (int i = 0; i < 5; i++) { + sentry_value_t crumb = sentry_value_new_breadcrumb(NULL, NULL); + sentry_value_set_by_key(crumb, "index", sentry_value_new_int32(i)); + sentry_add_breadcrumb(crumb); + } + sentry_attach_bytes("old", 3, "cleared-again.txt"); + sentry_clear_attachments(); + sentry_uuid_t removed = sentry_attach_bytes("old", 3, "removed.txt"); + sentry_attach_bytes("new", 3, "latest.txt"); + sentry_remove_attachment(removed); + SENTRY_WITH_OPTIONS (current) { + const char *names[] = { "__sentry-event", "__sentry-breadcrumb1", + "__sentry-breadcrumb2", "__sentry-attachments" }; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + sentry_path_t *path + = sentry__path_join_str(current->run->run_path, names[i]); + TEST_CHECK(!sentry__path_is_file(path)); + sentry__path_free(path); + } + if (crash_mode && !callback) { + // exercise staged IPC state without a crash-time snapshot + current->backend->except_func = NULL; + } + } + if (crash_mode) { +# if defined(SENTRY_PLATFORM_WINDOWS) + RaiseException(EXCEPTION_ACCESS_VIOLATION, 0, 0, NULL); +# else + raise(SIGSEGV); +# endif + } + sentry_span_finish(span); + sentry_transaction_finish(tx); + sentry_close(); +#endif +} + +#ifdef SENTRY_BACKEND_NATIVE +static sentry_crash_ipc_t * +new_test_ipc(void) +{ + sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_app(NULL); + TEST_ASSERT(ipc != NULL); + TEST_ASSERT(sentry__crash_scope_init(&ipc->scope, 2)); + sentry_value_t snapshot = scope_snapshot(); + TEST_ASSERT(sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, snapshot)); + sentry_value_decref(snapshot); + return ipc; +} + +static void +write_test_frame(sentry_crash_ipc_t *ipc, const char *buf, size_t len) +{ +# if defined(SENTRY_PLATFORM_WINDOWS) + DWORD written = 0; + TEST_ASSERT( + WriteFile(ipc->message_write_handle, buf, (DWORD)len, &written, NULL)); + TEST_CHECK_INT_EQUAL(written, len); +# else + TEST_CHECK_INT_EQUAL(write(ipc->message_fd[0], buf, len), len); +# endif +} +#endif + +SENTRY_TEST(crash_ipc_stream_ordering) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + sentry_value_t value = sentry_value_new_string("latest"); + TEST_CHECK(sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE, value)); + sentry_value_decref(value); + sentry__crash_ipc_notify(ipc); + TEST_CHECK(sentry__crash_ipc_receive(ipc, 100)); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + ipc->scope.event, "release")), + "latest"); + TEST_CHECK(!sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_null())); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_ipc_interrupted_frame) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + char *buf = NULL; + size_t len = 0; + const char payload[] = "\xa6latest"; + TEST_ASSERT( + sentry__crash_ipc_message_encode(SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE, + 0, 2, payload, sizeof(payload) - 1, &buf, &len) + == SENTRY_CRASH_IPC_MESSAGE_OK); + write_test_frame(ipc, buf, 7); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + write_test_frame(ipc, buf + 7, len - 8); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + ipc->writing = 1; + sentry__crash_ipc_notify(ipc); + TEST_CHECK(sentry__crash_ipc_receive(ipc, 100)); + TEST_CHECK_INT_EQUAL(ipc->scope.sequence, 1); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(ipc->scope.event, "release"))); + sentry_free(buf); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_ipc_partial_reads) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + char *buf = NULL; + size_t len = 0; + const char payload[] = "\xa6latest"; + TEST_ASSERT( + sentry__crash_ipc_message_encode(SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE, + 0, 2, payload, sizeof(payload) - 1, &buf, &len) + == SENTRY_CRASH_IPC_MESSAGE_OK); + for (size_t i = 0; i < len; i++) { + write_test_frame(ipc, buf + i, 1); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + TEST_CHECK_INT_EQUAL(ipc->scope.sequence, i + 1 == len ? 2 : 1); + } + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + ipc->scope.event, "release")), + "latest"); + sentry_free(buf); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_ipc_shutdown) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + TEST_CHECK(sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN, sentry_value_new_null())); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 100)); + TEST_CHECK(ipc->scope.stopped); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_ipc_broken_stream) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + char invalid[SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE] + = { 12, 0, 0, 0, (char)0xff, (char)0xff, 0, 0, 2 }; + write_test_frame(ipc, invalid, sizeof(invalid)); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + TEST_CHECK(ipc->message_closed); + TEST_CHECK_INT_EQUAL(ipc->scope.sequence, 1); + TEST_CHECK(!sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_null())); + sentry__crash_ipc_notify(ipc); + TEST_CHECK(sentry__crash_ipc_receive(ipc, 100)); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_ipc_crashing) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + sentry__atomic_store(&ipc->shmem->state, SENTRY_CRASH_STATE_CRASHING); + sentry__crash_ipc_notify(ipc); + TEST_CHECK(sentry__crash_ipc_receive(ipc, 100)); + TEST_CHECK(!ipc->scope.stopped); + TEST_CHECK(!ipc->notify_pending); + + sentry_value_t value = sentry_value_new_string("latest"); + TEST_CHECK(sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE, value)); + sentry_value_decref(value); + sentry__atomic_store(&ipc->shmem->state, SENTRY_CRASH_STATE_CRASHED); + sentry__crash_ipc_notify(ipc); + TEST_CHECK(sentry__crash_ipc_receive(ipc, 100)); + TEST_CHECK(ipc->scope.stopped); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + ipc->scope.event, "release")), + "latest"); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_daemon_shutdown_during_crash) +{ +#if !defined(SENTRY_BACKEND_NATIVE) || !defined(SENTRY_PLATFORM_LINUX) + SKIP_TEST(); +#else + const char *daemon = getenv("SENTRY_TEST_NATIVE_DAEMON"); + if (!daemon) { + SKIP_TEST(); + } + SENTRY_TEST_OPTIONS_NEW(options); + TEST_ASSERT(sentry__path_create_dir_all(options->database_path) == 0); + sentry_run_t *run = sentry__run_new(options->database_path); + TEST_ASSERT(run != NULL); + sentry_crash_ipc_t *ipc = new_test_ipc(); + snprintf(ipc->shmem->database_path, sizeof(ipc->shmem->database_path), "%s", + options->database_path->path); + snprintf(ipc->shmem->run_path, sizeof(ipc->shmem->run_path), "%s", + run->run_path->path); + ipc->shmem->event_discarded = true; + sentry__atomic_store(&ipc->shmem->state, SENTRY_CRASH_STATE_CRASHING); + TEST_ASSERT(sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN, sentry_value_new_null())); + + char pid[32], tid[32], notify[32], ready[32], message[32]; + snprintf(pid, sizeof(pid), "%d", (int)getpid()); + snprintf(tid, sizeof(tid), "%llx", (unsigned long long)pthread_self()); + snprintf(notify, sizeof(notify), "%d", ipc->notify_fd); + snprintf(ready, sizeof(ready), "%d", ipc->ready_fd); + snprintf(message, sizeof(message), "%d", ipc->message_fd[1]); + pid_t child = fork(); + TEST_ASSERT(child >= 0); + if (child == 0) { + int fds[] = { ipc->notify_fd, ipc->ready_fd, ipc->message_fd[1] }; + for (size_t i = 0; i < sizeof(fds) / sizeof(fds[0]); i++) { + fcntl(fds[i], F_SETFD, 0); + } + execl(daemon, daemon, pid, tid, notify, ready, message, NULL); + _exit(1); + } + TEST_CHECK(sentry__crash_ipc_wait_for_ready(ipc, 5000)); + sleep_ms(100); + TEST_CHECK_INT_EQUAL(waitpid(child, NULL, WNOHANG), 0); + + sentry__atomic_store(&ipc->shmem->state, SENTRY_CRASH_STATE_CRASHED); + sentry__crash_ipc_notify(ipc); + int status = 0; + pid_t result = 0; + for (int i = 0; i < 500; i++) { + result = waitpid(child, &status, WNOHANG); + if (result != 0) { + break; + } + sleep_ms(10); + } + if (result == 0) { + kill(child, SIGKILL); + waitpid(child, &status, 0); + } + TEST_CHECK_INT_EQUAL(result, child); + TEST_CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0); + TEST_CHECK_INT_EQUAL( + sentry__atomic_fetch(&ipc->shmem->state), SENTRY_CRASH_STATE_DONE); + sentry__crash_ipc_free(ipc); + sentry__run_free(run); + sentry_options_free(options); +#endif +} diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 8d312f583d..14417092e8 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1940,20 +1940,20 @@ observe_add_breadcrumb(void *data, sentry_value_t breadcrumb) } static void -observe_set_tag(void *data, const char *key, const char *value) +observe_set_tag(void *data, const char *key, sentry_value_t value) { test_observer_data_t *d = (test_observer_data_t *)data; if (sentry_value_is_null(d->tags)) { d->tags = sentry_value_new_object(); } - sentry_value_set_by_key(d->tags, key, sentry_value_new_string(value)); + sentry_value_set_by_key(d->tags, key, sentry_value_incref(value)); d->was_called = true; d->set_tag_count++; } static void observe_set_tag_remove_self_and_add( - void *data, const char *key, const char *value) + void *data, const char *key, sentry_value_t value) { reentrant_observer_data_t *d = (reentrant_observer_data_t *)data; observe_set_tag(d->self_data, key, value); @@ -1967,7 +1967,7 @@ observe_set_tag_remove_self_and_add( } static void -observe_set_tag_remove_self(void *data, const char *key, const char *value) +observe_set_tag_remove_self(void *data, const char *key, sentry_value_t value) { reentrant_observer_data_t *d = (reentrant_observer_data_t *)data; observe_set_tag(d->self_data, key, value); @@ -1977,7 +1977,7 @@ observe_set_tag_remove_self(void *data, const char *key, const char *value) } static void -observe_set_tag_clear_scope(void *data, const char *key, const char *value) +observe_set_tag_clear_scope(void *data, const char *key, sentry_value_t value) { clear_observer_data_t *d = (clear_observer_data_t *)data; observe_set_tag(d->data, key, value); @@ -1986,7 +1986,7 @@ observe_set_tag_clear_scope(void *data, const char *key, const char *value) static void observe_set_tag_mutate_nested_scope( - void *data, const char *UNUSED(key), const char *UNUSED(value)) + void *data, const char *UNUSED(key), sentry_value_t UNUSED(value)) { deferred_flush_observer_data_t *d = (deferred_flush_observer_data_t *)data; d->was_called = true; @@ -4377,3 +4377,157 @@ SENTRY_TEST(scope_parent_lock) sentry_scope_release(parent); sentry_scope_free(scope); } + +typedef struct { + sentry_scope_t *scope; + sentry_waitable_flag_t started; + sentry_waitable_flag_t finished; + sentry_waitable_flag_t read_finished; + sentry_threadid_t writer; + sentry_threadid_t reader; + volatile long calls; +} trace_notify_state_t; + +static void +observe_trace(void *data, sentry_value_t UNUSED(trace)) +{ + trace_notify_state_t *state = data; + sentry__atomic_fetch_and_add(&state->calls, 1); +} + +SENTRY_THREAD_FN +notify_scope_trace(void *data) +{ + trace_notify_state_t *state = data; + sentry__waitable_flag_set(&state->started); + sentry_scope_set_span(state->scope, NULL); + sentry__waitable_flag_set(&state->finished); + return 0; +} + +SENTRY_THREAD_FN +read_scope_during_notify(void *data) +{ + trace_notify_state_t *state = data; + sentry_value_decref(sentry__scope_load_tags(state->scope)); + sentry__waitable_flag_set(&state->read_finished); + return 0; +} + +static void +observe_tag_with_trace( + void *data, const char *UNUSED(key), sentry_value_t UNUSED(value)) +{ + trace_notify_state_t *state = data; + TEST_ASSERT( + !sentry__thread_spawn(&state->writer, notify_scope_trace, state)); + TEST_ASSERT(sentry__waitable_flag_wait(&state->started, 1000)); + TEST_CHECK(!sentry__waitable_flag_wait(&state->finished, 100)); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state->calls), 0); + + TEST_ASSERT( + !sentry__thread_spawn(&state->reader, read_scope_during_notify, state)); + TEST_CHECK(sentry__waitable_flag_wait(&state->read_finished, 1000)); +} + +SENTRY_TEST(scope_observer_trace_serialized) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + trace_notify_state_t state = { .scope = scope }; + sentry__waitable_flag_init(&state.started); + sentry__waitable_flag_init(&state.finished); + sentry__waitable_flag_init(&state.read_finished); + sentry__thread_init(&state.writer); + sentry__thread_init(&state.reader); + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + TEST_ASSERT(!!observer); + observer->data = &state; + observer->set_tag = observe_tag_with_trace; + observer->set_trace = observe_trace; + + TEST_ASSERT(sentry__scope_add_observer(scope, observer)); + sentry_scope_set_tag(scope, "tag", "value"); + + TEST_CHECK(sentry__waitable_flag_wait(&state.finished, 1000)); + sentry__thread_join(state.writer); + sentry__thread_join(state.reader); + sentry__thread_free(&state.writer); + sentry__thread_free(&state.reader); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.calls), 1); + sentry_scope_free(scope); +} + +static bool +init_trace_observer(void *data, const sentry_scope_t *scope) +{ + trace_notify_state_t *state = data; + TEST_CHECK(scope == state->scope); + TEST_ASSERT(scope->num_observers == 1); + TEST_CHECK(scope->observers[0]->data == data); + TEST_ASSERT( + !sentry__thread_spawn(&state->writer, notify_scope_trace, state)); + TEST_ASSERT(sentry__waitable_flag_wait(&state->started, 1000)); + TEST_CHECK(!sentry__waitable_flag_wait(&state->finished, 100)); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state->calls), 0); + + TEST_ASSERT( + !sentry__thread_spawn(&state->reader, read_scope_during_notify, state)); + TEST_CHECK(sentry__waitable_flag_wait(&state->read_finished, 1000)); + return true; +} + +SENTRY_TEST(scope_observer_init) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + trace_notify_state_t state = { .scope = scope }; + sentry__waitable_flag_init(&state.started); + sentry__waitable_flag_init(&state.finished); + sentry__waitable_flag_init(&state.read_finished); + sentry__thread_init(&state.writer); + sentry__thread_init(&state.reader); + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + TEST_ASSERT(!!observer); + observer->data = &state; + observer->set_trace = observe_trace; + + TEST_ASSERT(sentry__scope_add_observer_with_init( + scope, observer, init_trace_observer)); + + TEST_CHECK(sentry__waitable_flag_wait(&state.finished, 1000)); + sentry__thread_join(state.writer); + sentry__thread_join(state.reader); + sentry__thread_free(&state.writer); + sentry__thread_free(&state.reader); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.calls), 1); + sentry_scope_free(scope); +} + +static bool +reject_scope_observer(void *data, const sentry_scope_t *scope) +{ + TEST_CHECK_INT_EQUAL(scope->num_observers, 2); + *(bool *)data = true; + return false; +} + +SENTRY_TEST(scope_observer_init_failure) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + sentry_scope_observer_t *existing = sentry__scope_observer_new(); + TEST_ASSERT(!!existing); + TEST_ASSERT(sentry__scope_add_observer(scope, existing)); + + bool initialized = false; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + TEST_ASSERT(!!observer); + observer->data = &initialized; + TEST_CHECK(!sentry__scope_add_observer_with_init( + scope, observer, reject_scope_observer)); + TEST_CHECK(initialized); + TEST_CHECK_INT_EQUAL(scope->num_observers, 1); + TEST_CHECK(scope->observers[0] == existing); + sentry_scope_free(scope); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 4af1367126..e08f3e4fa6 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -142,13 +142,22 @@ XX(crash_context_init) XX(crash_context_null_options) XX(crash_context_options_propagation) XX(crash_context_transport_fields) +XX(crash_daemon_shutdown_during_crash) +XX(crash_ipc_broken_stream) +XX(crash_ipc_crashing) +XX(crash_ipc_interrupted_frame) XX(crash_ipc_message_invalid_lengths) XX(crash_ipc_message_large_roundtrip) XX(crash_ipc_message_oversized) XX(crash_ipc_message_partial) XX(crash_ipc_message_roundtrip) XX(crash_ipc_message_unknown_type) +XX(crash_ipc_partial_reads) +XX(crash_ipc_shutdown) +XX(crash_ipc_stream_ordering) XX(crash_marker) +XX(crash_scope_invalid_updates) +XX(crash_scope_updates) XX(crashed_last_run) XX(custom_logger) XX(daemon_adopts_existing_run) @@ -306,6 +315,7 @@ XX(mpack_newlines) XX(mpack_removed_tags) XX(multiple_inits) XX(multiple_transactions) +XX(native_scope_updates) XX(old_run_retry) XX(on_crashed_last_run) XX(on_crashed_last_run_cache) @@ -439,12 +449,15 @@ XX(scope_observer_deferred_flush) XX(scope_observer_environment) XX(scope_observer_extras) XX(scope_observer_fingerprint) +XX(scope_observer_init) +XX(scope_observer_init_failure) XX(scope_observer_level) XX(scope_observer_multiple) XX(scope_observer_mutate) XX(scope_observer_null) XX(scope_observer_release) XX(scope_observer_tags) +XX(scope_observer_trace_serialized) XX(scope_observer_transaction) XX(scope_observer_user) XX(scope_ownership) From e5f96150913f3d9e8bb152a806f942e0fd5925f4 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 19 Oct 2026 10:52:11 +0200 Subject: [PATCH 17/23] feat(internal): stdout transport (#2213) The inproc-based Android NDK uses `SENTRY_TRANSPORT=none` and installs a JNI function transport that writes to `outbox/`, leaving HTTP delivery to Java. Tests substitute it with an stdout callback to inspect envelopes without Java or HTTP. However, the Native daemon in #1725 cannot use the function transport from the separate daemon process. Provide (an internal) built-in stdout transport (`SENTRY_TRANSPORT=stdout`) that both the SDK and daemon can instantiate. This is mostly for out own integration testing purposes, but it's also super handy for quick experiments: ```sh $ make build SENTRY_BACKEND=inproc SENTRY_TRANSPORT=stdout ... $ ./build/sentry_example_crash ... [sentry] DEBUG sending envelope {"event_id":"0e1d5355-5ba8-42cb-9e96-c6610d4ee9c1",...} {"type":"event","length":2823} {"event_id":"0e1d5355-5ba8-42cb-9e96-c6610d4ee9c1","timestamp":"2026-10-16T16:19:26.968633Z","platform":"native","level":"fatal","exception":...} [sentry] INFO entering signal handler ... ``` --- CMakeLists.txt | 2 ++ src/CMakeLists.txt | 4 +++ src/transports/sentry_transport_stdout.c | 33 ++++++++++++++++++++++++ tests/test_integration_stdout.py | 4 +-- 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 src/transports/sentry_transport_stdout.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b180f63f7..8d967bfddd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -200,6 +200,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 61286232ad..2e408d784e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -145,6 +145,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..530bb10f6a --- /dev/null +++ b/src/transports/sentry_transport_stdout.c @@ -0,0 +1,33 @@ +#include "sentry_core.h" +#include "sentry_transport.h" + +#include + +#ifdef SENTRY_PLATFORM_WINDOWS +# include +# include +#endif + +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); + if (serialized) { + fwrite(serialized, 1, size_out, stdout); + fflush(stdout); + } + sentry_free(serialized); + sentry_envelope_free(envelope); +} + +sentry_transport_t * +sentry_transport_new_default(void) +{ +#ifdef SENTRY_PLATFORM_WINDOWS + fflush(stdout); + _setmode(_fileno(stdout), _O_BINARY); +#endif + return sentry_transport_new(print_envelope); +} diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index 0ea3c8a4c7..3212f1cfd6 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -40,14 +40,14 @@ def test_capture_stdout(cmake): ["sentry_test_integration"], { "SENTRY_BACKEND": "none", - "SENTRY_TRANSPORT": "none", + "SENTRY_TRANSPORT": "stdout", }, ) output = check_output( tmp_path, "sentry_test_integration", - ["stdout", "attachment", "capture-event", "add-stacktrace"], + ["attachment", "capture-event", "add-stacktrace"], ) envelope = Envelope.deserialize(output) From eaca2f949f2b7adedff901c5c13c891a8f0cc201 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 19 Oct 2026 12:00:54 +0200 Subject: [PATCH 18/23] fix(native): reduce daemon stack usage (#2211) * fix(native): Reduce daemon stack usage when writing minidumps Android e2e testing for #1725 exposed a daemon stack overflow: the roughly 1.2 MB local `minidump_writer_t` exceeds the crash service Java worker's stack, crashing the daemon before it writes a minidump. Allocate the writer on the heap to avoid exhausting the daemon's stack. https://github.com/getsentry/sentry-native/pull/1725 * consolidate cleanup_error, matching macos writer's convention --- CHANGELOG.md | 1 + .../native/minidump/sentry_minidump_linux.c | 110 ++++++++---------- 2 files changed, 50 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b0c2a928e..44a766d342 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ **Fixes**: - Native/Inproc: provide `debug_meta.images` in events passed to `on_crash`. ([#2184](https://github.com/getsentry/sentry-native/pull/2184)) +- Native/Linux: reduce daemon stack usage during minidump creation to avoid stack overflows in environments with limited stack space. ([#2211](https://github.com/getsentry/sentry-native/pull/2211)) ## 0.17.4 diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index 50a6115ac8..0fcd542cb0 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -2862,30 +2862,33 @@ sentry__write_minidump( SENTRY_DEBUGF("crashed_pid=%d, crashed_tid=%d, num_threads=%zu", ctx->crashed_pid, ctx->crashed_tid, ctx->platform.num_threads); - minidump_writer_t writer = { 0 }; - writer.crash_ctx = ctx; + minidump_writer_t *writer = SENTRY_MAKE(minidump_writer_t); + if (!writer) { + SENTRY_WARN("failed to allocate minidump writer"); + return -1; + } + writer->crash_ctx = ctx; // Open output file. O_RDWR (not O_WRONLY) because SMART-mode indirect // memory capture pread()'s thread-stack bytes back from the dump after // they're written, so it can scan them for heap pointers. - writer.fd = open(output_path, O_RDWR | O_CREAT | O_TRUNC, 0600); - if (writer.fd < 0) { + writer->fd = open(output_path, O_RDWR | O_CREAT | O_TRUNC, 0600); + if (writer->fd < 0) { SENTRY_WARNF("failed to create minidump: %s", strerror(errno)); + sentry_free(writer); return -1; } // Parse process information - if (parse_proc_maps(&writer) < 0 || enumerate_threads(&writer) < 0) { - close(writer.fd); - unlink(output_path); - return -1; + if (parse_proc_maps(writer) < 0 || enumerate_threads(writer) < 0) { + goto cleanup_error; } // Attach to crashed process via ptrace early so we can: // 1. Read memory using ptrace for memory list stream // 2. Get FPU state for crashed thread via PTRACE_GETFPREGS // 3. Get registers for threads with missing context via PTRACE_GETREGS - if (!ptrace_attach_process(&writer)) { + if (!ptrace_attach_process(writer)) { SENTRY_WARN( "Failed to attach to process via ptrace, continuing without " "it"); @@ -2917,20 +2920,15 @@ sentry__write_minidump( // fails the directory entry stays at type/size/rva = 0 (consumers ignore // it). const uint32_t stream_count = 14; - writer.current_offset = sizeof(minidump_header_t) + writer->current_offset = sizeof(minidump_header_t) + (stream_count * sizeof(minidump_directory_t)); SENTRY_DEBUGF("reserving space for %u streams, offset=%zu", stream_count, - writer.current_offset); + writer->current_offset); - if (lseek(writer.fd, writer.current_offset, SEEK_SET) < 0) { + if (lseek(writer->fd, writer->current_offset, SEEK_SET) < 0) { SENTRY_WARN("lseek failed"); - if (writer.ptrace_attached) { - ptrace(PTRACE_DETACH, ctx->crashed_tid, NULL, NULL); - } - close(writer.fd); - unlink(output_path); - return -1; + goto cleanup_error; } // Write streams @@ -2939,16 +2937,16 @@ sentry__write_minidump( int result = 0; SENTRY_DEBUG("writing system info stream"); - result |= write_system_info_stream(&writer, &directories[0]); + result |= write_system_info_stream(writer, &directories[0]); SENTRY_DEBUG("writing thread list stream"); - result |= write_thread_list_stream(&writer, &directories[1]); + result |= write_thread_list_stream(writer, &directories[1]); SENTRY_DEBUG("writing module list stream"); - result |= write_module_list_stream(&writer, &directories[2]); + result |= write_module_list_stream(writer, &directories[2]); SENTRY_DEBUG("writing exception stream"); - result |= write_exception_stream(&writer, &directories[3]); + result |= write_exception_stream(writer, &directories[3]); // Write memory list stream (empty for STACK_ONLY, populated for SMART/FULL) - result |= write_memory_list_stream(&writer, &directories[4]); + result |= write_memory_list_stream(writer, &directories[4]); // Write Linux-specific streams for debugger compatibility. // These are non-fatal: if they fail, the minidump is still valid. The @@ -2963,111 +2961,101 @@ sentry__write_minidump( } while (0) SENTRY_DEBUG("writing linux proc status stream"); - if (write_linux_proc_status_stream(&writer, &directories[5]) < 0) { + if (write_linux_proc_status_stream(writer, &directories[5]) < 0) { SENTRY_WARN("failed to write linux proc status stream"); NULLIFY_DIR_ENTRY(5, MINIDUMP_STREAM_LINUX_PROC_STATUS); } SENTRY_DEBUG("writing linux maps stream"); - if (write_linux_maps_stream(&writer, &directories[6]) < 0) { + if (write_linux_maps_stream(writer, &directories[6]) < 0) { SENTRY_WARN("failed to write linux maps stream"); NULLIFY_DIR_ENTRY(6, MINIDUMP_STREAM_LINUX_MAPS); } // Write thread names stream (matches Crashpad format) SENTRY_DEBUG("writing thread names stream"); - if (write_thread_names_stream(&writer, &directories[7]) < 0) { + if (write_thread_names_stream(writer, &directories[7]) < 0) { SENTRY_WARN("failed to write thread names stream"); NULLIFY_DIR_ENTRY(7, MINIDUMP_STREAM_THREAD_NAMES); } SENTRY_DEBUG("writing linux auxv stream"); - if (write_linux_auxv_stream(&writer, &directories[8]) < 0) { + if (write_linux_auxv_stream(writer, &directories[8]) < 0) { SENTRY_WARN("failed to write linux auxv stream"); NULLIFY_DIR_ENTRY(8, MINIDUMP_STREAM_LINUX_AUXV); } SENTRY_DEBUG("writing linux cpu info stream"); - if (write_linux_cpu_info_stream(&writer, &directories[9]) < 0) { + if (write_linux_cpu_info_stream(writer, &directories[9]) < 0) { SENTRY_DEBUG("linux cpu info stream unavailable"); NULLIFY_DIR_ENTRY(9, MINIDUMP_STREAM_LINUX_CPU_INFO); } SENTRY_DEBUG("writing linux lsb release stream"); - if (write_linux_lsb_release_stream(&writer, &directories[10]) < 0) { + if (write_linux_lsb_release_stream(writer, &directories[10]) < 0) { // Many distros don't ship /etc/lsb-release; this is expected. SENTRY_DEBUG("linux lsb release stream unavailable"); NULLIFY_DIR_ENTRY(10, MINIDUMP_STREAM_LINUX_LSB_RELEASE); } SENTRY_DEBUG("writing linux cmd line stream"); - if (write_linux_cmd_line_stream(&writer, &directories[11]) < 0) { + if (write_linux_cmd_line_stream(writer, &directories[11]) < 0) { SENTRY_DEBUG("linux cmd line stream unavailable"); NULLIFY_DIR_ENTRY(11, MINIDUMP_STREAM_LINUX_CMD_LINE); } SENTRY_DEBUG("writing linux environ stream"); - if (write_linux_environ_stream(&writer, &directories[12]) < 0) { + if (write_linux_environ_stream(writer, &directories[12]) < 0) { SENTRY_DEBUG("linux environ stream unavailable"); NULLIFY_DIR_ENTRY(12, MINIDUMP_STREAM_LINUX_ENVIRON); } SENTRY_DEBUG("writing linux dso debug stream"); - if (write_linux_dso_debug_stream(&writer, &directories[13]) < 0) { + if (write_linux_dso_debug_stream(writer, &directories[13]) < 0) { SENTRY_WARN("failed to write linux dso debug stream"); NULLIFY_DIR_ENTRY(13, MINIDUMP_STREAM_LINUX_DSO_DEBUG); } # undef NULLIFY_DIR_ENTRY if (result < 0) { - if (writer.ptrace_attached) { - ptrace(PTRACE_DETACH, ctx->crashed_tid, NULL, NULL); - } - close(writer.fd); - unlink(output_path); - return -1; + goto cleanup_error; } // Write header and directory at the beginning - if (lseek(writer.fd, 0, SEEK_SET) < 0) { - if (writer.ptrace_attached) { - ptrace(PTRACE_DETACH, ctx->crashed_tid, NULL, NULL); - } - close(writer.fd); - unlink(output_path); - return -1; + if (lseek(writer->fd, 0, SEEK_SET) < 0) { + goto cleanup_error; } - if (write_header(&writer, stream_count) < 0) { - if (writer.ptrace_attached) { - ptrace(PTRACE_DETACH, ctx->crashed_tid, NULL, NULL); - } - close(writer.fd); - unlink(output_path); - return -1; + if (write_header(writer, stream_count) < 0) { + goto cleanup_error; } // Write only the directory entries we actually used size_t dir_size = stream_count * sizeof(minidump_directory_t); - if (write(writer.fd, directories, dir_size) != (ssize_t)dir_size) { - if (writer.ptrace_attached) { - ptrace(PTRACE_DETACH, ctx->crashed_tid, NULL, NULL); - } - close(writer.fd); - unlink(output_path); - return -1; + if (write(writer->fd, directories, dir_size) != (ssize_t)dir_size) { + goto cleanup_error; } - close(writer.fd); + close(writer->fd); // Detach from process if we attached - if (writer.ptrace_attached) { + if (writer->ptrace_attached) { ptrace(PTRACE_DETACH, ctx->crashed_tid, NULL, NULL); SENTRY_DEBUGF("Detached from thread %d", ctx->crashed_tid); } + sentry_free(writer); SENTRY_DEBUG("successfully wrote minidump"); return 0; + +cleanup_error: + if (writer->ptrace_attached) { + ptrace(PTRACE_DETACH, ctx->crashed_tid, NULL, NULL); + } + close(writer->fd); + unlink(output_path); + sentry_free(writer); + return -1; } #endif // SENTRY_PLATFORM_LINUX || SENTRY_PLATFORM_ANDROID From 839f5102dd0ad7b38217103d7f409567cf46472f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 19 Oct 2026 13:26:58 +0200 Subject: [PATCH 19/23] fix(unwinder): Reduce maps buffer stack usage (#2212) * fix(unwinder): Reduce maps buffer stack usage #2174 brought the SDK unwinder's 4 KiB `/proc/self/maps` buffer onto the native signal-handler stack. Use 512 bytes; preserve oversized lines and address prefixes split across reads without new allocations. GCC 15 `-O3`, Linux x86-64, Ryzen AI 9 HX 370: | Measurement | 4096 B | 512 B | | --- | ---: | ---: | | Native handler peak (B) | 6,344 | 3,424 | | Maps lookups, 190 KiB / 1,900 mappings (ms) | 0.406 | 0.490 | | Maps lookups, 308 KiB / 3,053 mappings (ms) | 0.776 | 0.972 | This saves 2,920 bytes of handler stack at the cost of more reads. Lookup timings are warm code/stack lookup pairs: medians of nine rotating batches, excluding unwinding and other crash handling. * Update CHANGELOG.md --- CHANGELOG.md | 1 + src/unwinder/sentry_unwinder_libunwind.c | 2 +- tests/unit/test_unwinder.c | 21 +++++++++++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44a766d342..b223da2dd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Native/Inproc: provide `debug_meta.images` in events passed to `on_crash`. ([#2184](https://github.com/getsentry/sentry-native/pull/2184)) - Native/Linux: reduce daemon stack usage during minidump creation to avoid stack overflows in environments with limited stack space. ([#2211](https://github.com/getsentry/sentry-native/pull/2211)) +- Native/Linux: reduce stack usage when unwinding the crashed thread. ([#2212](https://github.com/getsentry/sentry-native/pull/2212)) ## 0.17.4 diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 883a85622f..2f06e08020 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -64,7 +64,7 @@ static bool find_mem_range_from_fd(int fd, uintptr_t ptr, mem_range_t *range) { - char buf[4096]; + char buf[512]; size_t carry = 0; bool skip_to_nl = false; // true while discarding an oversized line's tail diff --git a/tests/unit/test_unwinder.c b/tests/unit/test_unwinder.c index 1e6bf0ae7d..691ee22c49 100644 --- a/tests/unit/test_unwinder.c +++ b/tests/unit/test_unwinder.c @@ -195,12 +195,29 @@ SENTRY_TEST(find_mem_range) mem_range_t crafted_range = { 0, 0 }; found = find_mem_range_from_fd(fd, (uintptr_t)0xdead1000, &crafted_range); - close(fd); - unlink(tmp_path); TEST_CHECK(found); TEST_CHECK_PTR_EQUAL( (void *)crafted_range.lo, (void *)(uintptr_t)0xdead0000); TEST_CHECK_PTR_EQUAL( (void *)crafted_range.hi, (void *)(uintptr_t)0xdeadf000); + + // Test that the address prefix can span two 512-byte reads. + TEST_ASSERT(ftruncate(fd, 0) == 0); + lseek(fd, 0, SEEK_SET); + TEST_ASSERT(write(fd, skip_prefix, strlen(skip_prefix)) + == (ssize_t)strlen(skip_prefix)); + size_t padding = 507 - strlen(skip_prefix); + TEST_ASSERT(write(fd, filler, padding) == (ssize_t)padding); + TEST_ASSERT(write(fd, "\n", 1) == 1); + TEST_ASSERT(write(fd, target, strlen(target)) == (ssize_t)strlen(target)); + lseek(fd, 0, SEEK_SET); + + mem_range_t split_range = { 0, 0 }; + found = find_mem_range_from_fd(fd, (uintptr_t)0xdead1000, &split_range); + close(fd); + unlink(tmp_path); + TEST_CHECK(found); + TEST_CHECK_PTR_EQUAL((void *)split_range.lo, (void *)(uintptr_t)0xdead0000); + TEST_CHECK_PTR_EQUAL((void *)split_range.hi, (void *)(uintptr_t)0xdeadf000); #endif } From 65c9d1a62c5a397a0c6d9bf26fc75ddcc7ee6454 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 20 Oct 2026 14:41:51 +0200 Subject: [PATCH 20/23] fix(native): Preserve existing alternate signal stacks (#2219) * fix(native): Preserve existing alternate signal stacks Share the inproc signal-stack helpers through `sentry_os` and use them for native backend setup and cleanup. Reuse stacks installed by the runtime instead of replacing them with heap allocations that ASan tries to unmap when a thread exits. * test: Run unit tests with the native backend The unit suite was only ever built with `SENTRY_BACKEND=none`, so every test guarded by `SENTRY_BACKEND_NATIVE` in `test_native_backend.c` compiled to `SKIP_TEST()` in CI. Add a `test_unit_native` variant that builds `sentry_test_unit` together with `sentry-crash`, which `sentry_init` spawns for the native backend. * fix(native): Reduce crash handler signal stack usage Move `log_path` to static storage to reduce the handler's stack usage by 4 KiB and avoid overflowing existing 16 KiB signal stacks. The `handling_crash` guard prevents concurrent use of the buffer. --- CHANGELOG.md | 1 + src/backends/native/sentry_crash_handler.c | 31 +++---------- src/backends/sentry_backend_inproc.c | 54 ++-------------------- src/sentry_os.c | 51 +++++++++++++++++++- src/sentry_os.h | 21 +++++++++ tests/test_unit.py | 15 +++++- tests/unit/test_os.c | 43 +++++++++++++++++ tests/unit/tests.inc | 1 + 8 files changed, 142 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b223da2dd3..0e1bb93198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Native/Inproc: provide `debug_meta.images` in events passed to `on_crash`. ([#2184](https://github.com/getsentry/sentry-native/pull/2184)) - Native/Linux: reduce daemon stack usage during minidump creation to avoid stack overflows in environments with limited stack space. ([#2211](https://github.com/getsentry/sentry-native/pull/2211)) - Native/Linux: reduce stack usage when unwinding the crashed thread. ([#2212](https://github.com/getsentry/sentry-native/pull/2212)) +- Native/Unix: Preserve existing alternate signal stacks to prevent ASan aborts when the initializing thread exits before SDK shutdown. ([#2219](https://github.com/getsentry/sentry-native/pull/2219)) ## 0.17.4 diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index af52044678..6bd4c6613e 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -910,7 +910,8 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) // Dump daemon log for debugging (uses stdio, safe after page allocator // enabled) if (ctx && ctx->run_path[0] != '\0') { - char log_path[SENTRY_CRASH_MAX_PATH]; + // Static to save signal stack space (guarded by handling_crash) + static char log_path[SENTRY_CRASH_MAX_PATH]; const char suffix[] = "/sentry-daemon.log"; int len = 0; // Manually build path string (signal-safe) @@ -979,19 +980,9 @@ sentry__crash_handler_init( g_handler_strategy = strategy; // Set up signal stack - g_signal_stack.ss_sp = sentry_malloc(SENTRY_CRASH_SIGNAL_STACK_SIZE); - if (!g_signal_stack.ss_sp) { - SENTRY_WARN("failed to allocate signal stack"); - return -1; - } - - g_signal_stack.ss_size = SENTRY_CRASH_SIGNAL_STACK_SIZE; - g_signal_stack.ss_flags = 0; - - if (sigaltstack(&g_signal_stack, NULL) < 0) { - SENTRY_WARNF("failed to set signal stack: %s", strerror(errno)); - sentry_free(g_signal_stack.ss_sp); - g_signal_stack.ss_sp = NULL; + if (sentry__setup_sigaltstack( + &g_signal_stack, SENTRY_CRASH_SIGNAL_STACK_SIZE, "init") + < 0) { return -1; } @@ -1013,10 +1004,7 @@ sentry__crash_handler_init( sigaction(g_crash_signals[j], &g_previous_handlers[j], NULL); } // Clean up signal stack - g_signal_stack.ss_flags = SS_DISABLE; - sigaltstack(&g_signal_stack, NULL); - sentry_free(g_signal_stack.ss_sp); - g_signal_stack.ss_sp = NULL; + sentry__teardown_sigaltstack(&g_signal_stack); return -1; } installed_count++; @@ -1091,12 +1079,7 @@ sentry__crash_handler_shutdown(void) reset_signal_handlers(); // Clean up signal stack - if (g_signal_stack.ss_sp) { - g_signal_stack.ss_flags = SS_DISABLE; - sigaltstack(&g_signal_stack, NULL); - sentry_free(g_signal_stack.ss_sp); - g_signal_stack.ss_sp = NULL; - } + sentry__teardown_sigaltstack(&g_signal_stack); g_crash_ipc = NULL; diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index fd5949df33..ab10b05a9a 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -377,51 +377,6 @@ static const struct signal_slot SIGNAL_DEFINITIONS[SIGNAL_COUNT] = { static void handle_signal(int signum, siginfo_t *info, void *user_context); -/** - * Sets up an alternate signal stack for the current thread if one isn't - * already configured. The allocated stack is stored in `out_stack` so it - * can be freed later via `teardown_sigaltstack()`. - */ -static void -setup_sigaltstack(stack_t *out_stack, const char *context) -{ - memset(out_stack, 0, sizeof(*out_stack)); - - stack_t old_sig_stack; - int ret = sigaltstack(NULL, &old_sig_stack); - if (ret == 0 && old_sig_stack.ss_flags == SS_DISABLE) { - SENTRY_DEBUGF( - "installing %s sigaltstack (size: %d)", context, SIGNAL_STACK_SIZE); - out_stack->ss_sp = sentry_malloc(SIGNAL_STACK_SIZE); - if (!out_stack->ss_sp) { - SENTRY_WARN("failed to allocate signal stack"); - return; - } - out_stack->ss_size = SIGNAL_STACK_SIZE; - out_stack->ss_flags = 0; - sigaltstack(out_stack, 0); - } else if (ret == 0) { - SENTRY_DEBUGF("using existing signal stack (size: %d, flags: %d)", - old_sig_stack.ss_size, old_sig_stack.ss_flags); - } else if (ret == -1) { - SENTRY_WARNF("failed to query signal stack: %s", strerror(errno)); - } -} - -/** - * Tears down a signal stack previously set up via `setup_sigaltstack()`. - */ -static void -teardown_sigaltstack(stack_t *sig_stack) -{ - if (sig_stack->ss_sp) { - sig_stack->ss_flags = SS_DISABLE; - sigaltstack(sig_stack, 0); - sentry_free(sig_stack->ss_sp); - sig_stack->ss_sp = NULL; - } -} - static void reset_signal_handlers(void) { @@ -465,7 +420,7 @@ install_signal_handlers(void) } } - setup_sigaltstack(&g_signal_stack, "init"); + sentry__setup_sigaltstack(&g_signal_stack, SIGNAL_STACK_SIZE, "init"); sigemptyset(&g_sigaction.sa_mask); g_sigaction.sa_sigaction = handle_signal; @@ -533,7 +488,7 @@ shutdown_inproc_backend(sentry_backend_t *backend) // signal we still observe will fall through to the previously installed // handler. if (!sentry__atomic_fetch(&g_preloaded)) { - teardown_sigaltstack(&g_signal_stack); + sentry__teardown_sigaltstack(&g_signal_stack); reset_signal_handlers(); } @@ -876,7 +831,8 @@ handler_thread_main(void *UNUSED(data)) #ifdef SENTRY_PLATFORM_UNIX // Set up an alternate signal stack for the handler thread stack_t handler_thread_stack; - setup_sigaltstack(&handler_thread_stack, "handler"); + sentry__setup_sigaltstack( + &handler_thread_stack, SIGNAL_STACK_SIZE, "handler"); #endif signal_handler_ready(); @@ -939,7 +895,7 @@ handler_thread_main(void *UNUSED(data)) } #ifdef SENTRY_PLATFORM_UNIX - teardown_sigaltstack(&handler_thread_stack); + sentry__teardown_sigaltstack(&handler_thread_stack); #endif #ifdef SENTRY_PLATFORM_WINDOWS diff --git a/src/sentry_os.c b/src/sentry_os.c index 69b2390fd8..8bbab29c36 100644 --- a/src/sentry_os.c +++ b/src/sentry_os.c @@ -1,4 +1,5 @@ #include "sentry_os.h" +#include "sentry_logger.h" #include "sentry_path.h" #include "sentry_slice.h" #include "sentry_string.h" @@ -7,9 +8,12 @@ #include #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_WINDOWS) # include "sentry_core.h" -# include "sentry_logger.h" # include "sentry_utils.h" #endif +#if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_PS) +# include +# include +#endif #ifdef SENTRY_PLATFORM_LINUX # include #endif @@ -1263,3 +1267,48 @@ sentry__signal_to_string(int signum) } } #endif + +#if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_PS) +int +sentry__setup_sigaltstack(stack_t *out_stack, size_t size, const char *context) +{ + memset(out_stack, 0, sizeof(*out_stack)); + + stack_t old_sig_stack; + int ret = sigaltstack(NULL, &old_sig_stack); + if (ret == 0 && old_sig_stack.ss_flags == SS_DISABLE) { + SENTRY_DEBUGF("installing %s sigaltstack (size: %zu)", context, size); + out_stack->ss_sp = sentry_malloc(size); + if (!out_stack->ss_sp) { + SENTRY_WARN("failed to allocate signal stack"); + return -1; + } + out_stack->ss_size = size; + out_stack->ss_flags = 0; + if (sigaltstack(out_stack, 0) < 0) { + SENTRY_WARNF("failed to set signal stack: %s", strerror(errno)); + sentry_free(out_stack->ss_sp); + out_stack->ss_sp = NULL; + return -1; + } + } else if (ret == 0) { + SENTRY_DEBUGF("using existing signal stack (size: %d, flags: %d)", + old_sig_stack.ss_size, old_sig_stack.ss_flags); + } else if (ret == -1) { + SENTRY_WARNF("failed to query signal stack: %s", strerror(errno)); + return -1; + } + return 0; +} + +void +sentry__teardown_sigaltstack(stack_t *sig_stack) +{ + if (sig_stack->ss_sp) { + sig_stack->ss_flags = SS_DISABLE; + sigaltstack(sig_stack, 0); + sentry_free(sig_stack->ss_sp); + sig_stack->ss_sp = NULL; + } +} +#endif diff --git a/src/sentry_os.h b/src/sentry_os.h index 6be9dcb434..3c6f44691a 100644 --- a/src/sentry_os.h +++ b/src/sentry_os.h @@ -3,6 +3,10 @@ #include "sentry_boot.h" +#if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_PS) +# include +#endif + #ifdef SENTRY_PLATFORM_LINUX # include @@ -76,4 +80,21 @@ sentry_value_t sentry__build_registers(const sentry_ucontext_t *uctx); const char *sentry__signal_to_string(int signum); #endif +#if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_PS) +/** + * Sets up an alternate signal stack for the current thread if one isn't + * already configured. The allocated stack is stored in `out_stack` so it + * can be freed later via `sentry__teardown_sigaltstack()`. + * Returns 0 on success or -1 on failure. + */ +int sentry__setup_sigaltstack( + stack_t *out_stack, size_t size, const char *context); + +/** + * Tears down a signal stack previously set up via + * `sentry__setup_sigaltstack()`. + */ +void sentry__teardown_sigaltstack(stack_t *sig_stack); +#endif + #endif diff --git a/tests/test_unit.py b/tests/test_unit.py index 4f418fb9fd..e4f2401eeb 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -1,7 +1,7 @@ import os import pytest from . import run -from .conditions import has_http, is_wine +from .conditions import has_http, has_native, is_wine def _skip_if_unsupported(unittest): @@ -56,3 +56,16 @@ def test_unit_with_test_path(cmake, unittest): ) env = dict(os.environ) run(cwd, "sentry_test_unit", ["--no-summary", unittest], env=env) + + +@pytest.mark.skipif(not has_native, reason="tests need the native backend enabled") +def test_unit_native(cmake, unittest): + if unittest in ["basic_transport_thread_name", "cache_keep"]: + pytest.skip("excluded from unit test-suite") + _skip_if_unsupported(unittest) + cwd = cmake( + ["sentry_test_unit", "sentry-crash"], + {"SENTRY_BACKEND": "native", "SENTRY_TRANSPORT": "none"}, + ) + env = dict(os.environ) + run(cwd, "sentry_test_unit", ["--no-summary", unittest], env=env) diff --git a/tests/unit/test_os.c b/tests/unit/test_os.c index 7eacf43c1e..3c9e2a3437 100644 --- a/tests/unit/test_os.c +++ b/tests/unit/test_os.c @@ -372,3 +372,46 @@ SENTRY_TEST(stack_guarantee_auto_init) g_kernel32_GetCurrentThreadStackLimits = NULL; #endif } + +SENTRY_TEST(sigaltstack) +{ +#if !defined(SENTRY_PLATFORM_UNIX) || defined(SENTRY_PLATFORM_PS) + SKIP_TEST(); +#else + const size_t size = 64 * 1024; + stack_t old_stack; + TEST_ASSERT_INT_EQUAL(sigaltstack(NULL, &old_stack), 0); + stack_t disabled = { 0 }; + disabled.ss_size = size; + disabled.ss_flags = SS_DISABLE; + TEST_ASSERT_INT_EQUAL(sigaltstack(&disabled, NULL), 0); + + stack_t stack; + TEST_ASSERT_INT_EQUAL(sentry__setup_sigaltstack(&stack, size, "test"), 0); + TEST_CHECK(!!stack.ss_sp); + + stack_t current; + TEST_ASSERT_INT_EQUAL(sigaltstack(NULL, ¤t), 0); + TEST_CHECK(current.ss_sp == stack.ss_sp); + TEST_CHECK_INT_EQUAL(current.ss_size, size); + TEST_CHECK_INT_EQUAL(current.ss_flags, 0); + + stack_t other; + TEST_ASSERT_INT_EQUAL( + sentry__setup_sigaltstack(&other, size * 2, "test"), 0); + TEST_CHECK(!other.ss_sp); + sentry__teardown_sigaltstack(&other); + TEST_ASSERT_INT_EQUAL(sigaltstack(NULL, ¤t), 0); + TEST_CHECK(current.ss_sp == stack.ss_sp); + TEST_CHECK_INT_EQUAL(current.ss_size, size); + TEST_CHECK_INT_EQUAL(current.ss_flags, 0); + + sentry__teardown_sigaltstack(&stack); + TEST_CHECK(!stack.ss_sp); + TEST_ASSERT_INT_EQUAL(sigaltstack(NULL, ¤t), 0); + TEST_CHECK_INT_EQUAL(current.ss_flags, SS_DISABLE); + if (!(old_stack.ss_flags & SS_DISABLE)) { + TEST_ASSERT_INT_EQUAL(sigaltstack(&old_stack, NULL), 0); + } +#endif +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index e08f3e4fa6..07e0336cbf 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -496,6 +496,7 @@ XX(set_trace_id_with_txn) XX(set_trace_rebuilds_dsc_sample_rand) XX(set_trace_update_from_header) XX(set_trace_without_parent) +XX(sigaltstack) XX(slice) XX(slice_consume_uint64) XX(span_data) From 725ed34be4e8c53645c3fd3fc5808c27c2012d2b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 20 Oct 2026 16:50:43 +0200 Subject: [PATCH 21/23] feat(native): add basic crash handling to the daemon (#2220) * feat(native): add basic crash handling to the daemon Install fatal signal handlers on Unix and an unhandled exception filter and abort handler on Windows, following the in-process backend's conventions for signal context, recursion and chaining. Use these handlers to persist pending envelopes, including minidumps, for recovery on the next SDK start. * Update CHANGELOG.md * reuse sentry_crash_handler --- CHANGELOG.md | 1 + src/backends/native/sentry_crash_daemon.c | 44 +++++++++++++++++++++- src/backends/native/sentry_crash_handler.c | 43 ++++++++++++++++----- src/backends/native/sentry_crash_handler.h | 4 +- src/backends/sentry_backend_native.c | 5 ++- src/sentry_core.c | 11 ++++++ src/sentry_core.h | 6 +++ tests/unit/test_basic.c | 16 ++++++++ tests/unit/tests.inc | 1 + 9 files changed, 117 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d84c5226..e7ee5b1797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Add `sentry_scope_acquire` and `sentry_scope_release` to access the isolation scope. ([#2160](https://github.com/getsentry/sentry-native/pull/2160)) - Native: Provide exception metadata, registers, and a basic crashing-thread stack to `on_crash` for crash identification. ([#2174](https://github.com/getsentry/sentry-native/pull/2174)) - Native: reduce disk I/O on scope changes by sending incremental updates to the crash daemon over IPC. ([#2155](https://github.com/getsentry/sentry-native/pull/2155), [#2157](https://github.com/getsentry/sentry-native/pull/2157)) +- Native: install handlers for the daemon's own fatal signals and unhandled exceptions, allowing best-effort recovery actions such as dumping pending envelopes before termination. ([#2220](https://github.com/getsentry/sentry-native/pull/2220)) **Fixes**: diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 4dddcb1672..9bfc8cb860 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -4,6 +4,7 @@ #include "sentry_alloc.h" #include "sentry_attachment.h" #include "sentry_core.h" +#include "sentry_crash_handler.h" #include "sentry_crash_ipc.h" #include "sentry_database.h" #include "sentry_elf.h" @@ -20,6 +21,7 @@ #include "sentry_symbolizer.h" #include "sentry_sync.h" #include "sentry_transport.h" +#include "sentry_unix_pageallocator.h" #include "sentry_utils.h" #include "sentry_uuid.h" #include "sentry_value.h" @@ -4359,6 +4361,33 @@ daemon_file_logger( fflush(log_file); // Flush immediately to ensure logs are written } +static long g_crash_handling = 0; + +static void +dump_crash_queue(void) +{ + if (!sentry__atomic_compare_swap(&g_crash_handling, 0, 1)) { + return; + } + +#if defined(SENTRY_PLATFORM_UNIX) + (void)!sentry__enter_signal_handler(); + sentry__page_allocator_enable(); +#endif + sentry__logger_disable(); + + SENTRY_WITH_OPTIONS (options) { + if (options->run && options->transport) { + sentry__transport_dump_queue(options->transport, options->run); + sentry__http_transport_set_retry_enabled(options->transport, false); + } + } + +#if defined(SENTRY_PLATFORM_UNIX) + sentry__leave_signal_handler(); +#endif +} + #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, int notify_eventfd, @@ -4461,7 +4490,6 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, #endif // Initialize Sentry options for daemon (reuses all SDK infrastructure) - // Options are passed explicitly to all functions, no global state sentry_options_t *options = sentry_options_new(); if (!options) { SENTRY_ERROR("sentry_options_new() failed"); @@ -4597,6 +4625,15 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, SENTRY_WARN("No transport available"); } + sentry__options_set(options); + bool crash_handler_installed + = sentry__crash_handler_init( + NULL, SENTRY_HANDLER_STRATEGY_DEFAULT, dump_crash_queue) + == 0; + if (!crash_handler_installed) { + SENTRY_WARN("Failed to install daemon crash handler"); + } + SENTRY_DEBUG("Entering main loop"); // Daemon main loop @@ -4698,6 +4735,11 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, } } } + if (crash_handler_installed) { + sentry__crash_handler_shutdown(); + } + sentry__options_set(NULL); + if (crash_processed) { // Mark as done SENTRY_DEBUG("Marking crash state as DONE"); diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index 6bd4c6613e..db8ce22919 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -34,6 +34,8 @@ # include #endif +static void (*g_crash_callback)(void) = NULL; + #if (defined(SENTRY_PLATFORM_WINDOWS) \ && !((defined(_MSC_VER) || defined(__clang__)) \ && (defined(_M_AMD64) || defined(_M_ARM64)))) \ @@ -393,6 +395,9 @@ capture_stack(sentry_crash_capture_t *capture, ucontext_t *uctx) static void crash_signal_handler(int signum, siginfo_t *info, void *context) { + if (g_crash_callback) { + g_crash_callback(); + } sentry__enter_crash_handler(); ucontext_t *uctx = (ucontext_t *)context; @@ -969,15 +974,16 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) } int -sentry__crash_handler_init( - sentry_crash_ipc_t *ipc, sentry_handler_strategy_t strategy) +sentry__crash_handler_init(sentry_crash_ipc_t *ipc, + sentry_handler_strategy_t strategy, void (*callback)(void)) { - if (!ipc) { + if (!ipc && !callback) { return -1; } g_crash_ipc = ipc; g_handler_strategy = strategy; + g_crash_callback = callback; // Set up signal stack if (sentry__setup_sigaltstack( @@ -992,6 +998,9 @@ sentry__crash_handler_init( sigemptyset(&sa.sa_mask); sa.sa_sigaction = crash_signal_handler; sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + if (g_crash_callback) { + sa.sa_flags |= SA_NODEFER; + } size_t installed_count = 0; for (size_t i = 0; i < g_crash_signal_count; i++) { @@ -1046,7 +1055,7 @@ sentry__crash_handler_init( int sentry__crash_handler_reinstall(void) { - if (!g_crash_ipc) { + if (!g_crash_ipc && !g_crash_callback) { return -1; } @@ -1055,6 +1064,9 @@ sentry__crash_handler_reinstall(void) sigemptyset(&sa.sa_mask); sa.sa_sigaction = crash_signal_handler; sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + if (g_crash_callback) { + sa.sa_flags |= SA_NODEFER; + } for (size_t i = 0; i < g_crash_signal_count; i++) { struct sigaction previous; @@ -1082,6 +1094,7 @@ sentry__crash_handler_shutdown(void) sentry__teardown_sigaltstack(&g_signal_stack); g_crash_ipc = NULL; + g_crash_callback = NULL; SENTRY_DEBUG("crash handler shutdown"); } @@ -1169,6 +1182,16 @@ crash_sigabrt_handler(EXCEPTION_POINTERS *exception_pointers) static LONG WINAPI crash_exception_filter(EXCEPTION_POINTERS *exception_info) { + if (g_crash_callback) { + DWORD code = exception_info->ExceptionRecord->ExceptionCode; + if (code == EXCEPTION_BREAKPOINT || code == EXCEPTION_SINGLE_STEP) { + return EXCEPTION_CONTINUE_SEARCH; + } + g_crash_callback(); + SetUnhandledExceptionFilter(g_previous_filter); + return EXCEPTION_CONTINUE_SEARCH; + } + sentry__enter_crash_handler(); // Only handle crash once static volatile long handling_crash = 0; @@ -1254,15 +1277,16 @@ crash_exception_filter(EXCEPTION_POINTERS *exception_info) } int -sentry__crash_handler_init( - sentry_crash_ipc_t *ipc, sentry_handler_strategy_t strategy) +sentry__crash_handler_init(sentry_crash_ipc_t *ipc, + sentry_handler_strategy_t strategy, void (*callback)(void)) { - if (!ipc) { + if (!ipc && !callback) { return -1; } (void)strategy; g_crash_ipc = ipc; + g_crash_callback = callback; sentry__crash_handler_reinstall(); @@ -1273,7 +1297,7 @@ sentry__crash_handler_init( int sentry__crash_handler_reinstall(void) { - if (!g_crash_ipc) { + if (!g_crash_ipc && !g_crash_callback) { return -1; } @@ -1291,7 +1315,7 @@ void sentry__crash_handler_shutdown(void) { // Restore previous exception filter (including NULL) - if (g_crash_ipc) { + if (g_crash_ipc || g_crash_callback) { SetUnhandledExceptionFilter(g_previous_filter); g_previous_filter = NULL; } @@ -1299,6 +1323,7 @@ sentry__crash_handler_shutdown(void) sentry__win32_restore_sigabrt_handler(); g_crash_ipc = NULL; + g_crash_callback = NULL; SENTRY_DEBUG("crash handler shutdown"); } diff --git a/src/backends/native/sentry_crash_handler.h b/src/backends/native/sentry_crash_handler.h index bf54eed3df..a2c12054bc 100644 --- a/src/backends/native/sentry_crash_handler.h +++ b/src/backends/native/sentry_crash_handler.h @@ -7,8 +7,8 @@ /** * Initialize crash handler (install signal handlers) */ -int sentry__crash_handler_init( - sentry_crash_ipc_t *ipc, sentry_handler_strategy_t strategy); +int sentry__crash_handler_init(sentry_crash_ipc_t *ipc, + sentry_handler_strategy_t strategy, void (*callback)(void)); /** * Reinstalls crash handlers while retaining IPC and the signal stack. diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 32d266228f..3a486e1176 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -901,7 +901,8 @@ native_backend_startup( // Install crash handlers (signal handlers on Linux/macOS, Mach exception // handler on iOS) #if defined(SENTRY_PLATFORM_IOS) - if (sentry__crash_handler_init(state->ipc, SENTRY_HANDLER_STRATEGY_DEFAULT) + if (sentry__crash_handler_init( + state->ipc, SENTRY_HANDLER_STRATEGY_DEFAULT, NULL) < 0) { SENTRY_WARN("failed to initialize crash handler"); sentry__crash_ipc_free(state->ipc); @@ -1009,7 +1010,7 @@ native_backend_startup( options ? sentry_options_get_handler_strategy(options) : # endif SENTRY_HANDLER_STRATEGY_DEFAULT; - if (sentry__crash_handler_init(state->ipc, strategy) < 0) { + if (sentry__crash_handler_init(state->ipc, strategy, NULL) < 0) { SENTRY_WARN("failed to initialize crash handler"); # if defined(SENTRY_PLATFORM_UNIX) kill(state->daemon_pid, SIGTERM); diff --git a/src/sentry_core.c b/src/sentry_core.c index 1b6a5a4220..a3defcdf83 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -56,6 +56,17 @@ sentry__options_getref(void) return options; } +void +sentry__options_set(sentry_options_t *options) +{ + SENTRY__MUTEX_INIT_DYN_ONCE(g_options_lock); + sentry__mutex_lock(&g_options_lock); + sentry_options_t *previous = g_options; + g_options = sentry__options_incref(options); + sentry__mutex_unlock(&g_options_lock); + sentry_options_free(previous); +} + sentry_options_t * sentry__options_lock(void) { diff --git a/src/sentry_core.h b/src/sentry_core.h index c2b6eda2c2..fb18bdf4bc 100644 --- a/src/sentry_core.h +++ b/src/sentry_core.h @@ -161,6 +161,12 @@ sentry_value_t sentry__ensure_event_id( */ const sentry_options_t *sentry__options_getref(void); +/** + * Replaces the global options with a retained reference, or clears them if + * NULL. + */ +void sentry__options_set(sentry_options_t *options); + /** * This will acquire a lock on the global options. */ diff --git a/tests/unit/test_basic.c b/tests/unit/test_basic.c index 5d78c9fc8b..e6d474c469 100644 --- a/tests/unit/test_basic.c +++ b/tests/unit/test_basic.c @@ -865,3 +865,19 @@ SENTRY_TEST(attach_stacktrace_leaves_events_with_a_stacktrace_alone) sentry_value_decref(state.threads); } + +SENTRY_TEST(options_global_lifetime) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry__options_set(options); + TEST_CHECK_INT_EQUAL(options->refcount, 2); + + SENTRY_WITH_OPTIONS (current) { + TEST_CHECK_PTR_EQUAL(current, options); + sentry__options_set(NULL); + TEST_CHECK(!sentry_is_enabled()); + TEST_CHECK_INT_EQUAL(options->refcount, 2); + } + TEST_CHECK_INT_EQUAL(options->refcount, 1); + sentry_options_free(options); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 07e0336cbf..bcb87c99b3 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -323,6 +323,7 @@ XX(options_attach_stacktrace) XX(options_crash_reporting_mode_clamp) XX(options_crash_reporting_mode_default) XX(options_crash_reporting_mode_set_get) +XX(options_global_lifetime) XX(options_initial_scope_before_backend_startup) XX(options_initial_scope_replace) XX(options_initial_scope_rollback_after_startup_failure) From 27790f353b3bbef10b78f5d26c4be7fdf31e2adb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 20 Oct 2026 17:38:59 +0200 Subject: [PATCH 22/23] fix(native): group proc maps modules by load (#2228) * fix(native): group proc maps modules by load `capture_modules_from_proc_maps()` merged all mappings of a file into one module, producing oversized, overlapping images when a file is mapped more than once. This is common on Android, for example an APK that backs several native libraries. Keying by path and `start - offset` instead splits libraries whose segments differ in `p_vaddr - p_offset`, such as `libm.so.6` on x86_64 Linux. Start a new module at a mapping with offset 0 unless the file was captured last, which keeps small libraries that map every segment at offset 0 together. Other mappings extend the file's most recent module. Skip non-readable mappings and mappings whose offset exceeds their start address. Enable the `debug_meta` overlap check of the native crash tests on Linux. * Update CHANGELOG.md --- CHANGELOG.md | 1 + src/backends/native/sentry_crash_daemon.c | 22 ++++++++++++++++++---- tests/test_integration_native.py | 4 ++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ee5b1797..c0b67cba65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Native/Linux: reduce daemon stack usage during minidump creation to avoid stack overflows in environments with limited stack space. ([#2211](https://github.com/getsentry/sentry-native/pull/2211)) - Native/Linux: reduce stack usage when unwinding the crashed thread. ([#2212](https://github.com/getsentry/sentry-native/pull/2212)) - Native/Unix: Preserve existing alternate signal stacks to prevent ASan aborts when the initializing thread exits before SDK shutdown. ([#2219](https://github.com/getsentry/sentry-native/pull/2219)) +- Native/Linux: fix oversized `debug_meta.images` entries that overlapped other libraries when a file was mapped more than once. ([#2228](https://github.com/getsentry/sentry-native/pull/2228)) ## Unreleased diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 9bfc8cb860..07c4f34e03 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -1801,6 +1801,11 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) continue; // No pathname or special mapping like [stack] } + // Skip non-readable mappings + if (perms[0] != 'r') { + continue; + } + const char *pathname = line + pathname_offset; // Trim newline size_t len = strlen(pathname); @@ -1811,12 +1816,21 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) continue; } + if (offset > start) { + continue; + } + // Check if this file is already captured - if so, extend size if needed + // A mapping at offset 0 starts another load of the file, unless the + // file was captured last (small libraries can map all segments at + // offset 0) 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') { - existing_mod = &ctx->modules[j]; + for (uint32_t j = ctx->module_count; j > 0; j--) { + if (strncmp(ctx->modules[j - 1].name, pathname, len) == 0 + && ctx->modules[j - 1].name[len] == '\0') { + if (offset != 0 || j == ctx->module_count) { + existing_mod = &ctx->modules[j - 1]; + } break; } } diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index b62d66dc4c..8ff820d2e4 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -1446,7 +1446,7 @@ def test_crash_mode_native_only(cmake, httpserver): # Should have debug_meta assert "debug_meta" in event assert len(event["debug_meta"]["images"]) > 0 - if sys.platform == "darwin": + if sys.platform in ["linux", "darwin"] and not is_wine: assert_debug_meta_images_do_not_overlap(event) @@ -1500,7 +1500,7 @@ def test_crash_mode_native_with_minidump(cmake, httpserver): # Should have debug_meta assert "debug_meta" in event - if sys.platform == "darwin": + if sys.platform in ["linux", "darwin"] and not is_wine: assert_debug_meta_images_do_not_overlap(event) From 50a32b68f0dd9cf7254c508269578afe854ef126 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 20 Oct 2026 18:28:04 +0200 Subject: [PATCH 23/23] ref: Share `process_vm_readv` shim between Linux readers (#2227) Move the Android API < 23 syscall fallback from the module finder into `sentry__process_vm_readv()` in `sentry_os.c`, and use it in the module finder, the native minidump writer, and the crash daemon stack reader. --- .../native/minidump/sentry_minidump_linux.c | 9 ++++----- src/backends/native/sentry_crash_daemon.c | 4 ++-- src/modulefinder/sentry_modulefinder_linux.c | 15 ++------------- src/sentry_os.c | 19 +++++++++++++++++++ src/sentry_os.h | 11 +++++++++++ 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index 0fcd542cb0..39a2d5be97 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -27,6 +27,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. @@ -67,9 +68,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" @@ -347,7 +345,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; } @@ -2091,7 +2090,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 07c4f34e03..f27043ee13 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -907,8 +907,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 7d6b0d66ac..777bc385fb 100644 --- a/src/modulefinder/sentry_modulefinder_linux.c +++ b/src/modulefinder/sentry_modulefinder_linux.c @@ -5,6 +5,7 @@ #include "sentry_core.h" #include "sentry_elf.h" +#include "sentry_os.h" #include "sentry_path.h" #include "sentry_string.h" #include "sentry_sync.h" @@ -16,22 +17,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 @@ -137,7 +126,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 8bbab29c36..b6296dad73 100644 --- a/src/sentry_os.c +++ b/src/sentry_os.c @@ -15,9 +15,28 @@ # include #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 3c6f44691a..90bce1c847 100644 --- a/src/sentry_os.h +++ b/src/sentry_os.h @@ -7,6 +7,11 @@ # include #endif +#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +# include +# include +#endif + #ifdef SENTRY_PLATFORM_LINUX # include @@ -72,6 +77,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); sentry_value_t sentry__build_registers(const sentry_ucontext_t *uctx);