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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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); From 7aff784cae2860aa637193bb5df616472e3b6809 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 21 Oct 2026 10:28:28 +0200 Subject: [PATCH 24/47] chore!: remove deprecated APIs (#2230) Remove obsolete implementations and legacy user-report tests. List replacements and migration details in the v1 changelog. BREAKING CHANGE: Deprecated public APIs are no longer available. --- CHANGELOG.md | 12 ++ include/sentry.h | 137 --------------------- src/CMakeLists.txt | 1 - src/sentry_core.c | 62 ---------- src/sentry_envelope.c | 33 ----- src/sentry_envelope.h | 6 - src/sentry_info.c | 12 -- src/sentry_options.c | 46 ------- src/sentry_value.c | 44 ------- src/transports/sentry_function_transport.c | 42 ------- tests/assertions.py | 12 -- tests/fixtures/integration.c | 13 -- tests/test_integration_http.py | 29 ----- tests/unit/sentry_testsupport.h | 7 -- tests/unit/test_basic.c | 8 +- tests/unit/test_envelopes.c | 33 ----- tests/unit/test_value.c | 88 ------------- tests/unit/tests.inc | 3 - 18 files changed, 13 insertions(+), 575 deletions(-) delete mode 100644 src/transports/sentry_function_transport.c diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b67cba65..5e3f535dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ - 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)) +- Remove deprecated APIs. Migrate to the following alternatives: ([#2230](https://github.com/getsentry/sentry-native/pull/2230)) + - `sentry_new_function_transport`: use `sentry_transport_new` and `sentry_transport_set_state`. The send callback now receives a mutable envelope, takes ownership of it, and must free it with `sentry_envelope_free`. + - `sentry_event_value_add_stacktrace`: create a thread with `sentry_value_new_thread`, set its stack with `sentry_value_set_stacktrace`, and add it to the event with `sentry_event_add_thread`. + - `sentry_shutdown`: use `sentry_close`. + - `sentry_sdk_name` and `sentry_sdk_user_agent`: use `sentry_options_get_sdk_name` and `sentry_options_get_user_agent`, or `SENTRY_SDK_NAME` and `SENTRY_SDK_USER_AGENT` for build-time values. + - `sentry_options_set_http_proxy` and `sentry_options_set_http_proxy_n`: use `sentry_options_set_proxy` and `sentry_options_set_proxy_n`. + - `sentry_value_new_user_feedback` and `sentry_value_new_user_feedback_n`: use `sentry_value_new_feedback` and `sentry_value_new_feedback_n`. The constructor arguments are now message, contact email, name, and associated event ID, with lengths following each string in the `_n` variant. + - `sentry_capture_user_feedback`: use `sentry_capture_feedback`. + - `sentry_options_add_view_hierarchy`, `sentry_options_add_view_hierarchy_n`, `sentry_options_add_view_hierarchyw`, and `sentry_options_add_view_hierarchyw_n`: create an attachment with `sentry_attachment_from_file` or its `_n`/wide-string variants, set its type to `SENTRY_ATTACHMENT_TYPE_VIEW_HIERARCHY` with `sentry_attachment_set_type`, and add it with `sentry_add_attachment`. For startup configuration, use `sentry_scope_add_attachment` in a `sentry_options_set_initial_scope` callback. + - `sentry_capture_event_with_scope(event, scope)`: use `sentry_scope_capture_event(scope, event, NULL)`. + - `sentry_clear_crashed_last_run`: remove the call. `sentry_init` clears the crash marker automatically. + - `sentry_regenerate_trace`: use `sentry_start_new_trace`. **Features**: diff --git a/include/sentry.h b/include/sentry.h index cdce27c8bc..095f31959f 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -774,20 +774,6 @@ SENTRY_API sentry_value_t sentry_value_from_msgpack( SENTRY_API sentry_value_t sentry_value_from_msgpack_stream( const char *buf, size_t buf_len); -/** - * Adds a stack trace to an event. - * - * The stack trace is added as part of a new thread object. - * - * If `ips` is NULL, the current stack trace is captured. Otherwise, `len` - * stack trace instruction pointers are attached to the event. - */ -SENTRY_DEPRECATED( - "Use `sentry_value_new_stacktrace` in combination with " - "`sentry_value_new_thread` and `sentry_event_add_thread` instead") -SENTRY_EXPERIMENTAL_API void sentry_event_value_add_stacktrace( - sentry_value_t event, void **ips, size_t len); - /** * This represents the OS-dependent user context in the case of a crash and can * be used to manually capture a crash. @@ -1316,17 +1302,6 @@ SENTRY_EXPERIMENTAL_API void sentry_http_transport_set_client_shutdown_func( sentry_transport_t *transport, void (*shutdown_func)(sentry_http_client_t *client)); -/** - * Create a new function transport. - * - * It is a convenience function that works with a borrowed `data`, and will - * automatically free the envelope, so the user-provided function does not need - * to do that. - */ -SENTRY_DEPRECATED("Use `sentry_transport_new` instead") -SENTRY_API sentry_transport_t *sentry_new_function_transport( - void (*func)(const sentry_envelope_t *envelope, void *data), void *data); - /** * This represents an interface for user-defined backends. * @@ -1792,19 +1767,6 @@ SENTRY_API void sentry_options_set_proxy_n( */ SENTRY_API const char *sentry_options_get_proxy(const sentry_options_t *opts); -/** - * Configures the proxy. - * - * The given proxy has to include the full scheme, - * e.g., `http://some.proxy/. - */ -SENTRY_DEPRECATED("Use `sentry_options_set_proxy` instead") -SENTRY_API void sentry_options_set_http_proxy( - sentry_options_t *opts, const char *proxy); -SENTRY_DEPRECATED("Use `sentry_options_set_proxy_n` instead") -SENTRY_API void sentry_options_set_http_proxy_n( - sentry_options_t *opts, const char *proxy, size_t proxy_len); - /** * Returns the configured proxy. */ @@ -2061,26 +2023,6 @@ SENTRY_API void sentry_options_add_attachment( SENTRY_API void sentry_options_add_attachment_n( sentry_options_t *opts, const char *path, size_t path_len); -/** - * Adds a new view hierarchy attachment to be sent along. - * - * The primary use-case is for downstream SDKs (like sentry-godot). The - * view-hierarchy.json file should follow the representation defined in RFC#33 - * https://github.com/getsentry/rfcs/blob/main/text/0033-view-hierarchy.md - * - * `path` is assumed to be in platform-specific filesystem path encoding. - * API Users on windows are encouraged to use - * `sentry_options_add_view_hierarchyw` instead. - */ -SENTRY_DEPRECATED("Use `sentry_attach_*` with `sentry_attachment_set_type` and " - "`SENTRY_ATTACHMENT_TYPE_VIEW_HIERARCHY` instead") -SENTRY_API void sentry_options_add_view_hierarchy( - sentry_options_t *opts, const char *path); -SENTRY_DEPRECATED("Use `sentry_attach_*` with `sentry_attachment_set_type` and " - "`SENTRY_ATTACHMENT_TYPE_VIEW_HIERARCHY` instead") -SENTRY_API void sentry_options_add_view_hierarchy_n( - sentry_options_t *opts, const char *path, size_t path_len); - /** * Enables or disables attaching screenshots to fatal error events. Disabled by * default. @@ -2241,18 +2183,6 @@ SENTRY_API void sentry_options_add_attachmentw( SENTRY_API void sentry_options_add_attachmentw_n( sentry_options_t *opts, const wchar_t *path, size_t path_len); -/** - * Wide char version of `sentry_options_add_view_hierarchy`. - */ -SENTRY_DEPRECATED("Use `sentry_attach_*` with `sentry_attachment_set_type` and " - "`SENTRY_ATTACHMENT_TYPE_VIEW_HIERARCHY` instead") -SENTRY_API void sentry_options_add_view_hierarchyw( - sentry_options_t *opts, const wchar_t *path); -SENTRY_DEPRECATED("Use `sentry_attach_*` with `sentry_attachment_set_type` and " - "`SENTRY_ATTACHMENT_TYPE_VIEW_HIERARCHY` instead") -SENTRY_API void sentry_options_add_view_hierarchyw_n( - sentry_options_t *opts, const wchar_t *path, size_t path_len); - /** * Wide char version of `sentry_options_set_handler_path`. */ @@ -2555,14 +2485,6 @@ SENTRY_API int sentry_flush(uint64_t timeout); */ SENTRY_API int sentry_close(void); -/** - * Shuts down the sentry client and forces transports to flush out. - * - * Returns the number of envelopes that have been dumped. - */ -SENTRY_DEPRECATED("Use `sentry_close` instead") -SENTRY_API int sentry_shutdown(void); - /** * This will lazily load and cache a list of all the loaded libraries. * @@ -2798,15 +2720,6 @@ SENTRY_API sentry_uuid_t sentry_capture_event(sentry_value_t event); SENTRY_API sentry_uuid_t sentry_scope_capture_event( sentry_scope_t *scope, sentry_value_t event, sentry_hint_t *hint); -/** - * Deprecated alias for `sentry_scope_capture_event`. Note the reversed argument - * order: the replacement takes the scope first. This alias will be removed in - * 2027. - */ -SENTRY_DEPRECATED("Use `sentry_scope_capture_event` instead") -SENTRY_API sentry_uuid_t sentry_capture_event_with_scope( - sentry_value_t event, sentry_scope_t *scope); - /** * Allows capturing independently created minidumps. * @@ -3099,9 +3012,6 @@ SENTRY_API void sentry_set_trace_n(const char *trace_id, size_t trace_id_len, */ SENTRY_API void sentry_start_new_trace(void); -SENTRY_DEPRECATED("Use `sentry_start_new_trace` instead") -SENTRY_API void sentry_regenerate_trace(void); - /** * Sets the transaction. */ @@ -4305,28 +4215,6 @@ SENTRY_API void sentry_transaction_set_name( SENTRY_API void sentry_transaction_set_name_n( sentry_transaction_t *transaction, const char *name, size_t name_len); -/** - * Creates a deprecated User Report with a specific name, email, and comments. - * - * See - * https://develop.sentry.dev/sdk/data-model/envelope-items/#user-report---deprecated - */ -SENTRY_DEPRECATED("Use `sentry_value_new_feedback` instead") -SENTRY_API sentry_value_t sentry_value_new_user_feedback( - const sentry_uuid_t *uuid, const char *name, const char *email, - const char *comments); -SENTRY_DEPRECATED("Use `sentry_value_new_feedback_n` instead") -SENTRY_API sentry_value_t sentry_value_new_user_feedback_n( - const sentry_uuid_t *uuid, const char *name, size_t name_len, - const char *email, size_t email_len, const char *comments, - size_t comments_len); - -/** - * Captures a deprecated User Report and sends it to Sentry. - */ -SENTRY_DEPRECATED("Use `sentry_capture_feedback` instead") -SENTRY_API void sentry_capture_user_feedback(sentry_value_t user_report); - /** * Creates a new User Feedback with a specific message (required), and optional * contact_email, name, message, and associated_event_id. @@ -4586,19 +4474,6 @@ SENTRY_API void sentry_transaction_iter_headers(sentry_transaction_t *tx, */ SENTRY_API int sentry_get_crashed_last_run(void); -/** - * Clear the persisted status of the "crashed-last-run". - * - * Notes: - * * This doesn't change the value of sentry_get_crashed_last_run() yet. - * However, if sentry_init() is called again, the value will change. - * * This may only be called after sentry_init() and before sentry_close(). - * - * Returns 0 on success, 1 on error. - */ -SENTRY_DEPRECATED("The crash marker is cleared by `sentry_init()`.") -SENTRY_EXPERIMENTAL_API int sentry_clear_crashed_last_run(void); - /** * Returns the runtime SDK version as string. * @@ -4608,18 +4483,6 @@ SENTRY_EXPERIMENTAL_API int sentry_clear_crashed_last_run(void); */ SENTRY_API const char *sentry_sdk_version(void); -/** - * Sentry SDK name set during build time. - */ -SENTRY_DEPRECATED("Use `sentry_options_get_sdk_name` instead") -SENTRY_EXPERIMENTAL_API const char *sentry_sdk_name(void); - -/** - * Sentry SDK User-Agent set during build time. - */ -SENTRY_DEPRECATED("Use `sentry_options_get_user_agent` instead") -SENTRY_EXPERIMENTAL_API const char *sentry_sdk_user_agent(void); - #ifdef __cplusplus } #endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2e408d784e..11ee4f4c9b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -77,7 +77,6 @@ sentry_target_sources_cwd(sentry path/sentry_path.c screenshot/sentry_screenshot.c session_replay/sentry_session_replay.c - transports/sentry_function_transport.c transports/sentry_http_transport.c transports/sentry_http_transport.h unwinder/sentry_unwinder.c diff --git a/src/sentry_core.c b/src/sentry_core.c index a3defcdf83..4e47a56d7c 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -488,12 +488,6 @@ sentry_close(void) return (int)dumped_envelopes; } -int -sentry_shutdown(void) -{ - return sentry_close(); -} - int sentry_reinstall_backend(void) { @@ -711,12 +705,6 @@ sentry_scope_capture_event( } } -sentry_uuid_t -sentry_capture_event_with_scope(sentry_value_t event, sentry_scope_t *scope) -{ - return sentry_scope_capture_event(scope, event, NULL); -} - static sentry_value_t prepare_attachments( sentry_hint_t *hint, sentry_scope_t *local_scope); @@ -1044,24 +1032,6 @@ sentry__prepare_transaction(const sentry_options_t *options, return NULL; } -static sentry_envelope_t * -prepare_user_report(sentry_value_t user_report) -{ - sentry_envelope_t *envelope = NULL; - - envelope = sentry__envelope_new(); - if (!envelope || !sentry__envelope_add_user_report(envelope, user_report)) { - goto fail; - } - - return envelope; - -fail: - SENTRY_WARN("dropping user report"); - sentry_envelope_free(envelope); - return NULL; -} - static sentry_envelope_t * prepare_user_feedback(const sentry_options_t *options, sentry_value_t user_feedback, sentry_hint_t *hint, @@ -1552,12 +1522,6 @@ sentry_start_new_trace(void) } } -void -sentry_regenerate_trace(void) -{ - sentry_start_new_trace(); -} - void sentry_set_transaction(const char *transaction) { @@ -2011,20 +1975,6 @@ sentry_span_discard(sentry_span_t *opaque_span) sentry__span_decref(opaque_span); } -void -sentry_capture_user_feedback(sentry_value_t user_report) -{ - sentry_envelope_t *envelope = NULL; - - SENTRY_WITH_OPTIONS (options) { - envelope = prepare_user_report(user_report); - if (envelope) { - sentry__capture_envelope(options->transport, envelope, options); - } - } - sentry_value_decref(user_report); -} - static sentry_uuid_t capture_feedback(sentry_value_t user_feedback, sentry_hint_t *hint, sentry_scope_t *local_scope) @@ -2154,18 +2104,6 @@ sentry_get_crashed_last_run(void) return g_last_crash; } -int -sentry_clear_crashed_last_run(void) -{ - bool success = false; - sentry_options_t *options = sentry__options_lock(); - if (options) { - success = sentry__clear_crash_marker(options); - } - sentry__options_unlock(); - return success ? 0 : 1; -} - sentry_uuid_t sentry_capture_minidump(const char *path) { diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 92cb500b79..5357926d7e 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -632,39 +632,6 @@ sentry__envelope_add_metrics( "application/vnd.sentry.items.trace-metric+json"); } -sentry_envelope_item_t * -sentry__envelope_add_user_report( - sentry_envelope_t *envelope, sentry_value_t user_report) -{ - sentry_envelope_item_t *item = envelope_add_item(envelope); - if (!item) { - return NULL; - } - - sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL); - if (!jw) { - return NULL; - } - - sentry_uuid_t event_id; - sentry__ensure_event_id(user_report, &event_id); - - sentry__jsonwriter_write_value(jw, user_report); - item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); - if (!item->payload) { - return NULL; - } - - sentry__envelope_item_set_header( - item, "type", sentry_value_new_string("user_report")); - sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len); - sentry__envelope_item_set_header(item, "length", length); - - sentry__envelope_set_event_id(envelope, &event_id); - - return item; -} - sentry_envelope_item_t * sentry__envelope_add_feedback_event( sentry_envelope_t *envelope, sentry_value_t feedback_event) diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index e033d61788..42480f8f0b 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -66,12 +66,6 @@ sentry_envelope_item_t *sentry__envelope_add_event( sentry_envelope_item_t *sentry__envelope_add_transaction( sentry_envelope_t *envelope, sentry_value_t transaction); -/** - * Add a deprecated user report to this envelope. - */ -sentry_envelope_item_t *sentry__envelope_add_user_report( - sentry_envelope_t *envelope, sentry_value_t user_report); - /** * Add a list of logs to this envelope. */ diff --git a/src/sentry_info.c b/src/sentry_info.c index ca2f6387a4..12cdb7a046 100644 --- a/src/sentry_info.c +++ b/src/sentry_info.c @@ -5,15 +5,3 @@ sentry_sdk_version(void) { return SENTRY_SDK_VERSION; } - -const char * -sentry_sdk_name(void) -{ - return SENTRY_SDK_NAME; -} - -const char * -sentry_sdk_user_agent(void) -{ - return SENTRY_SDK_USER_AGENT; -} diff --git a/src/sentry_options.c b/src/sentry_options.c index db569319cd..2c8c4236bb 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -435,19 +435,6 @@ sentry_options_get_proxy(const sentry_options_t *opts) return opts->proxy; } -void -sentry_options_set_http_proxy_n( - sentry_options_t *opts, const char *proxy, size_t proxy_len) -{ - sentry_options_set_proxy_n(opts, proxy, proxy_len); -} - -void -sentry_options_set_http_proxy(sentry_options_t *opts, const char *proxy) -{ - sentry_options_set_proxy(opts, proxy); -} - const char * sentry_options_get_http_proxy(const sentry_options_t *opts) { @@ -808,21 +795,6 @@ sentry_options_add_attachment_n( &opts->attachments, path, path_len, NULL, NULL)); } -void -sentry_options_add_view_hierarchy(sentry_options_t *opts, const char *path) -{ - sentry_value_decref(sentry__attachments_add_path(&opts->attachments, path, - SENTRY_ATTACHMENT_TYPE_VIEW_HIERARCHY, "application/json")); -} - -void -sentry_options_add_view_hierarchy_n( - sentry_options_t *opts, const char *path, size_t path_len) -{ - sentry_value_decref(sentry__attachments_add_path_n(&opts->attachments, path, - path_len, SENTRY_ATTACHMENT_TYPE_VIEW_HIERARCHY, "application/json")); -} - void sentry_options_set_attach_screenshot(sentry_options_t *opts, int val) { @@ -912,24 +884,6 @@ sentry_options_add_attachmentw(sentry_options_t *opts, const wchar_t *path) sentry_options_add_attachmentw_n(opts, path, path_len); } -void -sentry_options_add_view_hierarchyw(sentry_options_t *opts, const wchar_t *path) -{ - size_t path_len = path ? wcslen(path) : 0; - sentry_value_decref( - sentry__attachments_add_wpath_n(&opts->attachments, path, path_len, - SENTRY_ATTACHMENT_TYPE_VIEW_HIERARCHY, "application/json")); -} - -void -sentry_options_add_view_hierarchyw_n( - sentry_options_t *opts, const wchar_t *path, size_t path_len) -{ - sentry_value_decref( - sentry__attachments_add_wpath_n(&opts->attachments, path, path_len, - SENTRY_ATTACHMENT_TYPE_VIEW_HIERARCHY, "application/json")); -} - void sentry_options_set_handler_pathw_n( sentry_options_t *opts, const wchar_t *path, size_t path_len) diff --git a/src/sentry_value.c b/src/sentry_value.c index b5c7710a10..dc51894dc1 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -1821,42 +1821,6 @@ sentry_value_new_stacktrace(void **ips, size_t len) return stacktrace; } -sentry_value_t -sentry_value_new_user_feedback(const sentry_uuid_t *uuid, const char *name, - const char *email, const char *comments) -{ - SENTRY_SUPPRESS_DEPRECATED - return sentry_value_new_user_feedback_n(uuid, name, - sentry__guarded_strlen(name), email, sentry__guarded_strlen(email), - comments, sentry__guarded_strlen(comments)); - SENTRY_RESTORE_DEPRECATED -} - -sentry_value_t -sentry_value_new_user_feedback_n(const sentry_uuid_t *uuid, const char *name, - size_t name_len, const char *email, size_t email_len, const char *comments, - size_t comments_len) -{ - sentry_value_t rv = sentry_value_new_object(); - - sentry_value_set_by_key(rv, "event_id", sentry__value_new_uuid(uuid)); - - if (name) { - sentry_value_set_by_key( - rv, "name", sentry_value_new_string_n(name, name_len)); - } - if (email) { - sentry_value_set_by_key( - rv, "email", sentry_value_new_string_n(email, email_len)); - } - if (comments) { - sentry_value_set_by_key( - rv, "comments", sentry_value_new_string_n(comments, comments_len)); - } - - return rv; -} - sentry_value_t sentry_value_new_feedback(const char *message, const char *contact_email, const char *name, const sentry_uuid_t *associated_event_id) @@ -1946,14 +1910,6 @@ sentry_value_set_stacktrace(sentry_value_t value, void **ips, size_t len) sentry_value_set_by_key(value, "stacktrace", stacktrace); } -void -sentry_event_value_add_stacktrace(sentry_value_t event, void **ips, size_t len) -{ - sentry_value_t thread = sentry_value_new_object(); - sentry_value_set_stacktrace(thread, ips, len); - sentry_event_add_thread(event, thread); -} - #define SENTRY_MPACK_MAX_DEPTH 64 static sentry_value_t diff --git a/src/transports/sentry_function_transport.c b/src/transports/sentry_function_transport.c deleted file mode 100644 index 5d801bf257..0000000000 --- a/src/transports/sentry_function_transport.c +++ /dev/null @@ -1,42 +0,0 @@ -#include "sentry_alloc.h" -#include "sentry_core.h" -#include "sentry_envelope.h" -#include "sentry_string.h" -#include "sentry_sync.h" - -struct transport_state { - void (*func)(const sentry_envelope_t *envelope, void *data); - void *data; -}; - -static void -send_envelope_function_transport(sentry_envelope_t *envelope, void *_state) -{ - struct transport_state *state = _state; - state->func(envelope, state->data); - sentry_envelope_free(envelope); -} - -sentry_transport_t * -sentry_new_function_transport( - void (*func)(const sentry_envelope_t *envelope, void *data), void *data) -{ - SENTRY_INFO("initializing function transport"); - struct transport_state *state = SENTRY_MAKE(struct transport_state); - if (!state) { - return NULL; - } - state->func = func; - state->data = data; - - sentry_transport_t *transport - = sentry_transport_new(send_envelope_function_transport); - if (!transport) { - sentry_free(state); - return NULL; - } - sentry_transport_set_state(transport, state); - sentry_transport_set_free_func(transport, sentry_free); - - return transport; -} diff --git a/tests/assertions.py b/tests/assertions.py index 183fa638b4..864506094e 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -64,18 +64,6 @@ def assert_user_feedback(envelope): assert user_feedback["message"] == "some-message" -def assert_user_report(envelope): - user_report = None - for item in envelope: - if item.headers.get("type") == "user_report" and item.payload.json is not None: - user_report = item.payload.json - - assert user_report is not None - assert user_report["name"] == "some-name" - assert user_report["email"] == "some-email" - assert user_report["comments"] == "some-comment" - - def assert_meta( envelope, release="test-example-release", diff --git a/tests/fixtures/integration.c b/tests/fixtures/integration.c index 8d81a66c7b..a3b1c7e25d 100644 --- a/tests/fixtures/integration.c +++ b/tests/fixtures/integration.c @@ -1505,19 +1505,6 @@ main(int argc, char **argv) // Clean up the temporary file remove(attachment_path); } - if (has_arg(argc, argv, "capture-user-report")) { - sentry_value_t event = sentry_value_new_message_event( - SENTRY_LEVEL_INFO, "my-logger", "Hello user feedback!"); - sentry_uuid_t event_id = sentry_capture_event(event); - - SENTRY_SUPPRESS_DEPRECATED - sentry_value_t user_feedback = sentry_value_new_user_feedback( - &event_id, "some-name", "some-email", "some-comment"); - - sentry_capture_user_feedback(user_feedback); - SENTRY_RESTORE_DEPRECATED - } - if (has_arg(argc, argv, "capture-transaction")) { sentry_transaction_context_t *tx_ctx = sentry_transaction_context_new("little.teapot", diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index fdbc22ab0e..55625d0966 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -30,7 +30,6 @@ assert_replay_envelope, assert_session, assert_user_feedback, - assert_user_report, assert_minidump, assert_breakpad_crash, assert_crash_timestamp, @@ -286,34 +285,6 @@ def test_user_feedback_with_attachments_http(cmake, httpserver): assert attachment_count == 2 -def test_user_report_http(cmake, httpserver): - tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) - - httpserver.expect_request( - "/api/123456/envelope/", - headers={"x-sentry-auth": auth_header}, - ).respond_with_data("OK") - env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) - - run( - tmp_path, - "sentry_test_integration", - ["log", "capture-user-report"], - env=env, - ) - - assert len(httpserver.log) == 2 - output = httpserver.log[0][0].get_data() - envelope = Envelope.deserialize(output) - - assert_event(envelope, "Hello user feedback!") - - output = httpserver.log[1][0].get_data() - envelope = Envelope.deserialize(output) - - assert_user_report(envelope) - - @pytest.mark.parametrize( "build_args", [ diff --git a/tests/unit/sentry_testsupport.h b/tests/unit/sentry_testsupport.h index 54c8bb2315..65aeaf5c81 100644 --- a/tests/unit/sentry_testsupport.h +++ b/tests/unit/sentry_testsupport.h @@ -104,13 +104,6 @@ sentry_dsn_t *Varname = sentry__dsn_new(DSN_URL); \ TEST_ASSERT(!!Varname) -#define SENTRY_TEST_DEPRECATED(call) \ - do { \ - SENTRY_SUPPRESS_DEPRECATED \ - call; \ - SENTRY_RESTORE_DEPRECATED \ - } while (0) - #ifdef SENTRY_PLATFORM_WINDOWS # include # define sleep_s(SECONDS) Sleep((SECONDS) * 1000) diff --git a/tests/unit/test_basic.c b/tests/unit/test_basic.c index e6d474c469..3dbac20ec5 100644 --- a/tests/unit/test_basic.c +++ b/tests/unit/test_basic.c @@ -206,10 +206,6 @@ SENTRY_TEST(crash_marker) SENTRY_TEST(crashed_last_run) { - // fails before init() is called - SENTRY_TEST_DEPRECATED( - TEST_CHECK_INT_EQUAL(sentry_clear_crashed_last_run(), 1)); - // clear any leftover from previous test runs { SENTRY_TEST_OPTIONS_NEW(options); @@ -262,9 +258,7 @@ SENTRY_TEST(crashed_last_run) TEST_CHECK_INT_EQUAL(sentry_get_crashed_last_run(), 1); TEST_CHECK(sentry__has_crash_marker(options)); - // explicit clearing remains supported on all platforms - SENTRY_TEST_DEPRECATED( - TEST_CHECK_INT_EQUAL(sentry_clear_crashed_last_run(), 0)); + TEST_CHECK(sentry__clear_crash_marker(options)); sentry_close(); } diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index 2641b6c79d..8475754a23 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -96,39 +96,6 @@ SENTRY_TEST(basic_http_request_preparation_for_transaction) sentry__dsn_decref(dsn); } -SENTRY_TEST(basic_http_request_preparation_for_user_report) -{ - SENTRY_TEST_DSN_NEW_DEFAULT(dsn); - - sentry_uuid_t event_id - = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); - sentry_envelope_t *envelope = sentry__envelope_new(); - sentry_value_t user_report; - SENTRY_TEST_DEPRECATED( - user_report = sentry_value_new_user_feedback( - &event_id, "some-name", "some-email", "some-comment")); - sentry__envelope_add_user_report(envelope, user_report); - - sentry_prepared_http_request_t *req - = sentry__prepare_http_request(envelope, dsn, NULL, NULL); - TEST_CHECK_STRING_EQUAL(req->method, "POST"); - TEST_CHECK_STRING_EQUAL( - req->url, "https://sentry.invalid:443/api/42/envelope/"); -#ifndef SENTRY_TRANSPORT_COMPRESSION - TEST_CHECK_STRING_EQUAL(req->body, - "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\"}\n" - "{\"type\":\"user_report\",\"length\":117}\n" - "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"name\":" - "\"some-name\",\"email\":\"some-email\",\"comments\":" - "\"some-comment\"}"); -#endif - sentry__prepared_http_request_free(req); - sentry_value_decref(user_report); - sentry_envelope_free(envelope); - - sentry__dsn_decref(dsn); -} - SENTRY_TEST(basic_http_request_preparation_for_user_feedback) { SENTRY_TEST_DSN_NEW_DEFAULT(dsn); diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index f366275b11..ea6261c18c 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -1685,94 +1685,6 @@ SENTRY_TEST(thread_without_name_still_valid) sentry_value_decref(thread); } -SENTRY_TEST(user_report_is_valid) -{ - sentry_uuid_t event_id - = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); - sentry_value_t user_feedback; - SENTRY_TEST_DEPRECATED( - user_feedback = sentry_value_new_user_feedback( - &event_id, "some-name", "some-email", "some-comment")); - - TEST_CHECK(!sentry_value_is_null(user_feedback)); - TEST_CHECK_STRING_EQUAL( - sentry_value_as_string(sentry_value_get_by_key(user_feedback, "name")), - "some-name"); - TEST_CHECK_STRING_EQUAL( - sentry_value_as_string(sentry_value_get_by_key(user_feedback, "email")), - "some-email"); - TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( - user_feedback, "comments")), - "some-comment"); - - sentry_value_decref(user_feedback); -} - -SENTRY_TEST(user_feedback_with_null_args) -{ - sentry_uuid_t event_id - = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); - sentry_value_t user_feedback; - SENTRY_TEST_DEPRECATED(user_feedback - = sentry_value_new_user_feedback(&event_id, NULL, NULL, NULL)); - - TEST_CHECK(!sentry_value_is_null(user_feedback)); - TEST_CHECK( - sentry_value_is_null(sentry_value_get_by_key(user_feedback, "name"))); - TEST_CHECK( - sentry_value_is_null(sentry_value_get_by_key(user_feedback, "email"))); - TEST_CHECK(sentry_value_is_null( - sentry_value_get_by_key(user_feedback, "comments"))); - - sentry_value_decref(user_feedback); - - SENTRY_TEST_DEPRECATED(user_feedback = sentry_value_new_user_feedback( - &event_id, NULL, "some-email", "some-comment")); - - TEST_CHECK(!sentry_value_is_null(user_feedback)); - TEST_CHECK( - sentry_value_is_null(sentry_value_get_by_key(user_feedback, "name"))); - TEST_CHECK_STRING_EQUAL( - sentry_value_as_string(sentry_value_get_by_key(user_feedback, "email")), - "some-email"); - TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( - user_feedback, "comments")), - "some-comment"); - - sentry_value_decref(user_feedback); - - SENTRY_TEST_DEPRECATED(user_feedback = sentry_value_new_user_feedback( - &event_id, "some-name", NULL, "some-comment")); - - TEST_CHECK(!sentry_value_is_null(user_feedback)); - TEST_CHECK_STRING_EQUAL( - sentry_value_as_string(sentry_value_get_by_key(user_feedback, "name")), - "some-name"); - TEST_CHECK( - sentry_value_is_null(sentry_value_get_by_key(user_feedback, "email"))); - TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( - user_feedback, "comments")), - "some-comment"); - - sentry_value_decref(user_feedback); - - SENTRY_TEST_DEPRECATED(user_feedback = sentry_value_new_user_feedback( - &event_id, "some-name", "some-email", NULL)); - - TEST_CHECK(!sentry_value_is_null(user_feedback)); - - TEST_CHECK_STRING_EQUAL( - sentry_value_as_string(sentry_value_get_by_key(user_feedback, "name")), - "some-name"); - TEST_CHECK_STRING_EQUAL( - sentry_value_as_string(sentry_value_get_by_key(user_feedback, "email")), - "some-email"); - TEST_CHECK(sentry_value_is_null( - sentry_value_get_by_key(user_feedback, "comments"))); - - sentry_value_decref(user_feedback); -} - SENTRY_TEST(user_feedback_is_valid) { sentry_uuid_t event_id diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index bcb87c99b3..fb231a5125 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -49,7 +49,6 @@ XX(basic_http_request_preparation_for_event_with_attachment) XX(basic_http_request_preparation_for_minidump) XX(basic_http_request_preparation_for_transaction) XX(basic_http_request_preparation_for_user_feedback) -XX(basic_http_request_preparation_for_user_report) XX(basic_logging_functionality) XX(basic_spans) XX(basic_tracing_context) @@ -567,8 +566,6 @@ XX(url_parsing_no_path) XX(url_parsing_partial) XX(url_parsing_with_path) XX(user_feedback_is_valid) -XX(user_feedback_with_null_args) -XX(user_report_is_valid) XX(uuid_api) XX(uuid_is_valid) XX(uuid_v4) From 0eef20cf49b1199026f1ea8d344b87fd6f0d6604 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 22 Oct 2026 20:35:58 +0200 Subject: [PATCH 25/47] ci: mitigate Android emulator setup failures (#2231) The Android API 35 job failed before emulator startup: > Warning: An error occurred while preparing SDK package Google APIs Intel > x86_64 Atom System Image: dl.google.com. > ... > error: could not connect to TCP port 5554: Connection refused https://github.com/getsentry/sentry-native/actions/runs/39443426624/job/118698972826 Retry SDK installation and emulator startup failures once while keeping failures after boot fatal on the original test step. Use smaller AOSP images to reduce setup downloads. The tests do not use Google Play services, and the API 35 x86_64 image is about 780 MB instead of 1.7 GB. Both variants still come from `dl.google.com`, so retries remain necessary for transient download failures. --- .github/workflows/ci.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d22d1ee7eb..a6001d9186 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -501,14 +501,17 @@ jobs: - name: Test (Android) if: ${{ env['ANDROID_API'] }} + id: android-test + continue-on-error: ${{ env.ANDROID_BOOTED != 'true' }} uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 - with: + with: &android-options api-level: ${{ env.ANDROID_API }} ndk: ${{ env.ANDROID_NDK }} arch: ${{ env.ANDROID_ARCH }} - target: google_apis + target: default emulator-boot-timeout: 1200 script: | + echo "ANDROID_BOOTED=true" >> "$GITHUB_ENV" # Sync emulator clock with host to avoid timestamp assertion failures adb shell cmd alarm set-time $(date +%s000) || adb shell su root date $(date -u +%m%d%H%M%Y.%S) scripts/setup-android-sanitizer.sh "$RUN_ANALYZER" @@ -516,6 +519,11 @@ jobs: pytest --capture=no --verbose tests (cd ndk && ./gradlew :sentry-native-ndk:connectedDebugAndroidTest --stacktrace) + - name: Retry emulator (Android) + if: ${{ steps.android-test.outcome == 'failure' && env.ANDROID_BOOTED != 'true' }} + uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 + with: *android-options + - name: Test if: ${{ !env['ANDROID_API'] }} shell: bash From c450877928f8ef34cf68b6e1e4c229e8de650b60 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 22 Oct 2026 22:54:50 +0200 Subject: [PATCH 26/47] fix(native): use localtime_r in the daemon file logger The daemon logs from both its main thread and the transport worker thread, but `daemon_file_logger` used `localtime`, which shares a static result buffer and re-runs `tzset` on every call. Under TSAN, the resulting race report in the worker thread could deadlock against the main thread priming libunwind inside `dl_iterate_phdr`, hanging the daemon before it entered its main loop. Keep `localtime` on Windows, where the CRT result is thread-local. --- src/backends/native/sentry_crash_daemon.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index f27043ee13..44a296875a 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -4361,7 +4361,12 @@ daemon_file_logger( // Get current timestamp time_t now = time(NULL); +#if defined(SENTRY_PLATFORM_WINDOWS) struct tm *tm_info = localtime(&now); +#else + struct tm tm_buf; + struct tm *tm_info = localtime_r(&now, &tm_buf); +#endif char timestamp[SENTRY_CRASH_TIMESTAMP_SIZE]; strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info); From 712b8c26d5911bcff19ba6f2866400d68d0d94d9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 23 Oct 2026 08:17:58 +0200 Subject: [PATCH 27/47] test: stabilize flaky transaction timestamp checks (#2233) `sentry__usec_time_to_iso8601` omits the fractional part when the microseconds are zero, which is valid RFC 3339. The transaction tests parsed timestamps with a fixed `%Y-%m-%dT%H:%M:%S.%fZ` format and failed whenever a timestamp landed exactly on a whole second. Parse them with `datetime.fromisoformat` instead, as `assertions.py` already does. --- tests/test_integration_transactions.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_integration_transactions.py b/tests/test_integration_transactions.py index 410715e7b4..85af227922 100644 --- a/tests/test_integration_transactions.py +++ b/tests/test_integration_transactions.py @@ -1,6 +1,6 @@ import os -import time import uuid +from datetime import datetime import pytest @@ -20,7 +20,6 @@ pytestmark = pytest.mark.skipif(not has_http, reason="tests need http") -RFC3339_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" UPSTREAM_TRACE_ID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" UPSTREAM_PARENT_SPAN_ID = "bbbbbbbbbbbbbbbb" @@ -85,9 +84,9 @@ def test_transaction_only(cmake, httpserver, build_args): assert trace_context["span_id"] assert trace_context["status"] == "ok" - start_timestamp = time.strptime(payload["start_timestamp"], RFC3339_FORMAT) + start_timestamp = datetime.fromisoformat(payload["start_timestamp"]) assert start_timestamp - timestamp = time.strptime(payload["timestamp"], RFC3339_FORMAT) + timestamp = datetime.fromisoformat(payload["timestamp"]) assert timestamp >= start_timestamp assert trace_context["data"] == {"url": "https://example.com"} @@ -141,9 +140,9 @@ def test_before_transaction_callback(cmake, httpserver): assert trace_context["span_id"] assert trace_context["status"] == "ok" - start_timestamp = time.strptime(payload["start_timestamp"], RFC3339_FORMAT) + start_timestamp = datetime.fromisoformat(payload["start_timestamp"]) assert start_timestamp - timestamp = time.strptime(payload["timestamp"], RFC3339_FORMAT) + timestamp = datetime.fromisoformat(payload["timestamp"]) assert timestamp >= start_timestamp assert trace_context["data"] == {"url": "https://example.com"} From a6afbb18268e9aa47b4586e8d9a61ed97bc4251a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 23 Oct 2026 09:25:13 +0200 Subject: [PATCH 28/47] test: make unit tests work out of the box with CTest (#2234) Deploy the selected crash handler beside `sentry_test_unit` so unit-only builds work with the default hierarchical output layout. Run CTest unit cases serially because they share filesystem fixtures. Allow `make test-unit` and `make test-leaks` to select `SENTRY_BACKEND` and `SENTRY_TRANSPORT`, preserving `none` as the default backend. This allows resuing the convenient ready-made targets for any backend and transport combination. Close: #2215 --- Makefile | 6 ++++-- tests/unit/CMakeLists.txt | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8685a12d3c..5826afba73 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,8 @@ test-unit: update-test-discovery CMakeLists.txt @cd unit-build; cmake \ -DCMAKE_RUNTIME_OUTPUT_DIRECTORY=$(PWD)/unit-build \ -DCMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO=$(PWD)/unit-build \ - -DSENTRY_BACKEND=none \ + -DSENTRY_BACKEND=$(or $(SENTRY_BACKEND),none) \ + $(if $(SENTRY_TRANSPORT),-DSENTRY_TRANSPORT=$(SENTRY_TRANSPORT)) \ .. @cmake --build unit-build --target sentry_test_unit --parallel --config RelWithDebInfo ./unit-build/sentry_test_unit @@ -39,7 +40,8 @@ test-leaks: update-test-discovery CMakeLists.txt @mkdir -p leak-build @cd leak-build; cmake \ -DCMAKE_RUNTIME_OUTPUT_DIRECTORY=$(PWD)/leak-build \ - -DSENTRY_BACKEND=none \ + -DSENTRY_BACKEND=$(or $(SENTRY_BACKEND),none) \ + $(if $(SENTRY_TRANSPORT),-DSENTRY_TRANSPORT=$(SENTRY_TRANSPORT)) \ -DWITH_ASAN_OPTION=ON \ -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang \ -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++ \ diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 719a7c1c07..cb4203f924 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -107,6 +107,14 @@ target_compile_definitions(sentry PRIVATE SIZEOF_LONG=${CMAKE_SIZEOF_LONG}) target_compile_definitions(sentry_test_unit PRIVATE SENTRY_UNITTEST) +foreach(handler crashpad::handler sentry-crash) + if(TARGET ${handler}) + add_custom_command(TARGET sentry_test_unit POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$" + VERBATIM) + endif() +endforeach() + option(SENTRY_CTEST_INDIVIDUAL "Register sentry unit tests as individual tests instead of a single binary" ON) if(SENTRY_CTEST_INDIVIDUAL) @@ -119,10 +127,12 @@ if(SENTRY_CTEST_INDIVIDUAL) message(WARNING "Failed to extract test name from line: ${line}") else() add_test(NAME sentry/${CMAKE_MATCH_1} COMMAND sentry_test_unit --no-summary ${CMAKE_MATCH_1}) + set_tests_properties(sentry/${CMAKE_MATCH_1} PROPERTIES RUN_SERIAL TRUE) endif() endforeach() else() add_test(NAME sentry/unit-tests COMMAND sentry_test_unit) + set_tests_properties(sentry/unit-tests PROPERTIES RUN_SERIAL TRUE) endif() add_executable(sentry_fuzz_json From 0fdaebc6599e5ff0a5f01c0bb65b4c880cf0a8eb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 26 Oct 2026 13:07:17 +0100 Subject: [PATCH 29/47] feat: add `sentry_scope_set_trace` (#2236) * feat: add `sentry_scope_set_trace` Add `sentry_scope_set_trace()` and `sentry_scope_set_trace_n()` to set the trace on a given scope, so that a local scope can carry a distinct trace for a concurrent unit of work. The global `sentry_set_trace_n()` now forwards to the scope-level setter. * fix: use the capture scope's DSC for event envelopes Scoped event and feedback capture apply the given scope to the event payload, but `sentry__envelope_add_event()` read the dynamic sampling context (DSC) and the propagation context's `sample_rand` from the isolation scope through `SENTRY_WITH_SCOPE`. Since `sentry_scope_set_trace()` lets a local scope carry its own propagation context and DSC, such an envelope mixed metadata from two scopes: - the event's trace context came from the local scope, - the header's `trace_id` matched, because it is copied from the event payload, - `sample_rand`, `sampled`, `release` and `environment` came from the isolation scope. For example, with a `traces_sample_rate` of 0.5, an isolation scope with a `sample_rand` of 0.25 and a local scope with 0.75, scoped capture sent `sampled: "true"` instead of the local scope's `"false"`. Feedback was affected the same way, because `sentry__envelope_add_feedback_event()` delegates to `sentry__envelope_add_event()`. Other Sentry SDKs avoid this by computing the DSC from the scope that is applied to the event and carrying it on the event as SDK-internal data, which the envelope builder moves into the header: - sentry-go stores it in `event.sdkMetaData.dsc` from `Scope.ApplyToEvent`, - sentry-php stores it in the event's SDK metadata from `Scope::applyToEvent`, - sentry-javascript stores it in `event.sdkProcessingMetadata`, - sentry-dotnet stores it in an internal `SentryEvent.DynamicSamplingContext`, - sentry-python stores it in `contexts.trace.dynamic_sampling_context`, which the client pops before building the envelope. In all of them the envelope is a plain container that never looks up a scope. Follow the same pattern: when the new `SENTRY_SCOPE_DSC` mode is set, `sentry__scope_apply_to_event()` stamps the DSC on non-transaction events under an internal `dsc` key, and `sentry__envelope_add_event()` removes it from the payload and uses it as the `trace` header. The envelope no longer reads any scope or options. The mode is opt-in because not every scope-applied event goes through `sentry__envelope_add_event()`. Crashpad's handler uploads the on-disk `__sentry-event` directly, and the native backend's daemon writes its own envelopes. Relay reports unknown top-level event attributes as invalid, so those paths apply their scopes without the mode. `SENTRY_SCOPE_ALL` includes it, which covers `sentry__prepare_event()`, and feedback requests it explicitly. As a result, crashpad envelopes built from reports in a later run no longer get a `trace` header from that run's isolation scope, which never described the crash. * test: add integration test for scoped trace propagation Capture an event with a local scope whose trace is set with `sentry_scope_set_trace()`, while the isolation scope carries a different trace, and check that both the event's trace context and the envelope `trace` header come from the local scope. The header's `sample_rand` must match the event's. Before the DSC was taken from the capture scope, it came from the isolation scope instead. * Update CHANGELOG.md --- CHANGELOG.md | 1 + include/sentry.h | 5 ++ src/sentry_core.c | 46 +--------- src/sentry_core.h | 2 - src/sentry_envelope.c | 31 +------ src/sentry_scope.c | 73 ++++++++++++++++ src/sentry_scope.h | 2 + src/sentry_value.c | 39 +++++++-- src/sentry_value.h | 6 ++ tests/fixtures/integration.c | 6 ++ tests/test_integration_transactions.py | 44 ++++++++++ tests/unit/test_envelopes.c | 5 +- tests/unit/test_scope.c | 114 +++++++++++++++++++++++++ tests/unit/tests.inc | 2 + 14 files changed, 291 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e3f535dcb..7b4dd69d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - 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)) +- Add `sentry_scope_set_trace` for per-scope traces. ([#2236](https://github.com/getsentry/sentry-native/pull/2236)) **Fixes**: diff --git a/include/sentry.h b/include/sentry.h index 095f31959f..f0bf7b3f2f 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2996,6 +2996,11 @@ SENTRY_API void sentry_set_trace( const char *trace_id, const char *parent_span_id); SENTRY_API void sentry_set_trace_n(const char *trace_id, size_t trace_id_len, const char *parent_span_id, size_t parent_span_id_len); +SENTRY_API void sentry_scope_set_trace( + sentry_scope_t *scope, const char *trace_id, const char *parent_span_id); +SENTRY_API void sentry_scope_set_trace_n(sentry_scope_t *scope, + const char *trace_id, size_t trace_id_len, const char *parent_span_id, + size_t parent_span_id_len); /** * Generates a new random `trace_id` and `span_id` and sets these onto diff --git a/src/sentry_core.c b/src/sentry_core.c index 4e47a56d7c..c0bd374bab 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1047,11 +1047,11 @@ prepare_user_feedback(const sentry_options_t *options, if (local_scope) { sentry__scope_apply_to_event( - local_scope, options, event, SENTRY_SCOPE_NONE); + local_scope, options, event, SENTRY_SCOPE_DSC); } else { SENTRY_WITH_SCOPE (scope) { sentry__scope_apply_to_event( - scope, options, event, SENTRY_SCOPE_NONE); + scope, options, event, SENTRY_SCOPE_DSC); } } @@ -1382,14 +1382,6 @@ sentry_update_context_n(const char *key, size_t key_len, sentry_value_t value) } } -void -sentry__set_propagation_context(const char *key, sentry_value_t value) -{ - SENTRY_WITH_SCOPE_MUT (scope) { - sentry__scope_set_propagation_context(scope, key, value); - } -} - void sentry__apply_to_telemetry(const sentry_scope_t *scope, const sentry_options_t *options, sentry_value_t telemetry, @@ -1473,39 +1465,9 @@ void sentry_set_trace_n(const char *trace_id, size_t trace_id_len, const char *parent_span_id, size_t parent_span_id_len) { - sentry_value_t context = sentry_value_new_null(); - SENTRY_WITH_SCOPE_MUT (scope) { - context = sentry_value_new_object(); - - sentry_value_set_by_key( - context, "type", sentry_value_new_string("trace")); - - sentry_value_set_by_key(context, "trace_id", - sentry_value_new_string_n(trace_id, trace_id_len)); - if (parent_span_id && parent_span_id_len) { - sentry_value_set_by_key(context, "parent_span_id", - sentry_value_new_string_n(parent_span_id, parent_span_id_len)); - } - - sentry_uuid_t span_id = sentry_uuid_new_v4(); - sentry_value_set_by_key( - context, "span_id", sentry__value_new_span_uuid(&span_id)); - sentry__scope_set_trace_managed(scope, false); - sentry__scope_flush(scope); - } - - if (!sentry_value_is_null(context)) { - sentry__generate_sample_rand(context); - - sentry__set_propagation_context("trace", context); - - SENTRY_WITH_OPTIONS (options) { - SENTRY_WITH_SCOPE_MUT (scope) { - sentry__scope_update_dsc(scope, options); - sentry__scope_flush(scope); - } - } + sentry_scope_set_trace_n( + scope, trace_id, trace_id_len, parent_span_id, parent_span_id_len); } } diff --git a/src/sentry_core.h b/src/sentry_core.h index fb18bdf4bc..19924ab956 100644 --- a/src/sentry_core.h +++ b/src/sentry_core.h @@ -177,8 +177,6 @@ sentry_options_t *sentry__options_lock(void); */ void sentry__options_unlock(void); -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 diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 5357926d7e..ebe81ea6aa 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -13,7 +13,6 @@ #include "sentry_value.h" #include #include -#include #include struct sentry_envelope_item_s { @@ -439,9 +438,12 @@ sentry__envelope_add_event(sentry_envelope_t *envelope, sentry_value_t event) sentry_uuid_t event_id; sentry__ensure_event_id(event, &event_id); + sentry_value_t dsc = sentry__value_take_by_key(event, "dsc"); + sentry__jsonwriter_write_value(jw, event); item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); if (!item->payload) { + sentry_value_decref(dsc); return NULL; } item->event = event; @@ -453,19 +455,6 @@ sentry__envelope_add_event(sentry_envelope_t *envelope, sentry_value_t event) sentry__envelope_set_event_id(envelope, &event_id); - double traces_sample_rate = 0.0; - SENTRY_WITH_OPTIONS (options) { - traces_sample_rate = options->traces_sample_rate; - } - sentry_value_t dsc = sentry_value_new_null(); - double sample_rand = (double)NAN; - SENTRY_WITH_SCOPE (scope) { - dsc = sentry__scope_load_dsc(scope); - sentry_value_t trace_context = sentry__scope_load_trace_context(scope); - sample_rand = sentry_value_as_double( - sentry_value_get_by_key(trace_context, "sample_rand")); - sentry_value_decref(trace_context); - } if (!sentry_value_is_null(dsc)) { sentry_value_t trace_id = sentry_value_get_by_key( sentry_value_get_by_key( @@ -478,20 +467,6 @@ sentry__envelope_add_event(sentry_envelope_t *envelope, sentry_value_t event) SENTRY_WARN("couldn't retrieve trace_id from scope to apply to the " "dynamic sampling context"); } - if (!isnan(sample_rand)) { - if (sample_rand >= traces_sample_rate) { - sentry_value_set_by_key( - dsc, "sampled", sentry_value_new_string("false")); - } else { - sentry_value_set_by_key( - dsc, "sampled", sentry_value_new_string("true")); - } - } else { - // only for testing; in production, the SDK should always have a - // non-null sample_rand. We don't set "sampled" to keep dsc empty - SENTRY_WARN("couldn't retrieve sample_rand from scope to apply to " - "the dynamic sampling context"); - } // only add dsc if it has values if (sentry_value_is_true(dsc)) { #ifdef SENTRY_UNITTEST diff --git a/src/sentry_scope.c b/src/sentry_scope.c index c305b1372a..29afd2b7dd 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -16,6 +16,7 @@ #include "sentry_uuid.h" #include "sentry_value.h" +#include #include #ifdef SENTRY_BACKEND_CRASHPAD @@ -1278,6 +1279,33 @@ sentry__scope_load_dsc(const sentry_scope_t *scope) return dsc; } +static sentry_value_t +make_dsc(const sentry_scope_t *scope, const sentry_options_t *options) +{ + sentry_value_t dsc = sentry__scope_load_dsc(scope); + sentry_value_t trace_context = sentry__scope_load_trace_context(scope); + double sample_rand = sentry_value_as_double( + sentry_value_get_by_key(trace_context, "sample_rand")); + sentry_value_decref(trace_context); + if (!sentry_value_is_null(dsc)) { + if (!isnan(sample_rand)) { + if (sample_rand >= options->traces_sample_rate) { + sentry_value_set_by_key( + dsc, "sampled", sentry_value_new_string("false")); + } else { + sentry_value_set_by_key( + dsc, "sampled", sentry_value_new_string("true")); + } + } else { + // only for testing; in production, the SDK should always have a + // non-null sample_rand. We don't set "sampled" to keep dsc empty + SENTRY_WARN("couldn't retrieve sample_rand from scope to apply to " + "the dynamic sampling context"); + } + } + return dsc; +} + void sentry__scope_foreach_dsc(const sentry_scope_t *scope, sentry_value_foreach_key_value_function_t callback, void *userdata) @@ -1497,6 +1525,7 @@ sentry__scope_apply_to_event(const sentry_scope_t *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_value_t dsc = sentry_value_new_null(); if (!is_transaction) { scope_trace = load_span_or_transaction_trace_context(scope); @@ -1505,6 +1534,9 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, sentry_value_get_by_key(event_contexts, "trace"))) { propagation_context = sentry__scope_load_propagation_context(scope); } + if (mode & SENTRY_SCOPE_DSC) { + dsc = make_dsc(scope, options); + } } if (mode & SENTRY_SCOPE_BREADCRUMBS) { scope_breadcrumbs = sentry__scope_breadcrumbs_to_list(scope); @@ -1571,6 +1603,9 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, PLACE_VALUE("sdk", client_sdk); sentry_value_decref(client_sdk); + PLACE_VALUE("dsc", dsc); + sentry_value_decref(dsc); + if (is_transaction && !sentry_value_is_null(contexts)) { sentry_value_remove_by_key(contexts, "trace"); } @@ -2325,6 +2360,44 @@ sentry_scope_remove_fingerprint(sentry_scope_t *scope) sentry__scope_flush(scope); } +void +sentry_scope_set_trace( + sentry_scope_t *scope, const char *trace_id, const char *parent_span_id) +{ + sentry_scope_set_trace_n(scope, trace_id, sentry__guarded_strlen(trace_id), + parent_span_id, sentry__guarded_strlen(parent_span_id)); +} + +void +sentry_scope_set_trace_n(sentry_scope_t *scope, const char *trace_id, + size_t trace_id_len, const char *parent_span_id, size_t parent_span_id_len) +{ + sentry_value_t context = sentry_value_new_object(); + + sentry_value_set_by_key(context, "type", sentry_value_new_string("trace")); + + sentry_value_set_by_key( + context, "trace_id", sentry_value_new_string_n(trace_id, trace_id_len)); + if (parent_span_id && parent_span_id_len) { + sentry_value_set_by_key(context, "parent_span_id", + sentry_value_new_string_n(parent_span_id, parent_span_id_len)); + } + + sentry_uuid_t span_id = sentry_uuid_new_v4(); + sentry_value_set_by_key( + context, "span_id", sentry__value_new_span_uuid(&span_id)); + sentry__scope_set_trace_managed(scope, false); + + sentry__generate_sample_rand(context); + + sentry__scope_set_propagation_context(scope, "trace", context); + + SENTRY_WITH_OPTIONS (options) { + sentry__scope_update_dsc(scope, options); + sentry__scope_flush(scope); + } +} + sentry_level_t sentry__scope_get_level(const sentry_scope_t *scope) { diff --git a/src/sentry_scope.h b/src/sentry_scope.h index b3e2777324..c6e63fe4d6 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -89,6 +89,8 @@ typedef enum { SENTRY_SCOPE_MODULES = 0x2, // Symbolize all the stacktraces on-device which are found in the event. SENTRY_SCOPE_STACKTRACES = 0x4, + // Add the dynamic sampling context for the envelope trace header. + SENTRY_SCOPE_DSC = 0x8, // All of the above. SENTRY_SCOPE_ALL = ~0, } sentry_scope_mode_t; diff --git a/src/sentry_value.c b/src/sentry_value.c index dc51894dc1..8333cdc824 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -849,32 +849,53 @@ sentry_value_set_by_key(sentry_value_t value, const char *k, sentry_value_t v) return 1; } -char * -sentry__value_remove_and_take_key_n( - sentry_value_t value, const char *k, size_t k_len) +static bool +take_pair(sentry_value_t value, const char *k, size_t k_len, obj_pair_t *out) { if (!k) { - return NULL; + return false; } sentry_slice_t k_slice = { k, k_len }; thing_t *thing = value_as_unfrozen_thing(value); if (!thing || thing_get_type(thing) != THING_TYPE_OBJECT || !thing_detach(thing)) { - return NULL; + return false; } obj_t *o = thing->payload._ptr; for (size_t i = 0; i < o->len; i++) { obj_pair_t *pair = &o->pairs[i]; if (sentry__slice_eqs(k_slice, pair->k)) { - char *key = pair->k; - sentry_value_decref(pair->v); + *out = *pair; memmove(o->pairs + i, o->pairs + i + 1, (o->len - i - 1) * sizeof(o->pairs[0])); o->len--; - return key; + return true; } } - return NULL; + return false; +} + +char * +sentry__value_remove_and_take_key_n( + sentry_value_t value, const char *k, size_t k_len) +{ + obj_pair_t pair; + if (!take_pair(value, k, k_len, &pair)) { + return NULL; + } + sentry_value_decref(pair.v); + return pair.k; +} + +sentry_value_t +sentry__value_take_by_key(sentry_value_t value, const char *k) +{ + obj_pair_t pair; + if (!k || !take_pair(value, k, strlen(k), &pair)) { + return sentry_value_new_null(); + } + sentry_free(pair.k); + return pair.v; } int diff --git a/src/sentry_value.h b/src/sentry_value.h index 0ec6830b10..edaf42dead 100644 --- a/src/sentry_value.h +++ b/src/sentry_value.h @@ -92,6 +92,12 @@ void sentry__value_replace(sentry_value_t *target, sentry_value_t value); char *sentry__value_remove_and_take_key_n( sentry_value_t value, const char *key, size_t key_len); +/** + * Removes a value by key and returns it, transferring ownership to the + * caller. Returns a null value if the key does not exist. + */ +sentry_value_t sentry__value_take_by_key(sentry_value_t value, const char *key); + /** * This will parse the Value into a UUID, or return a `nil` UUID on error. * See also `sentry_uuid_from_string`. diff --git a/tests/fixtures/integration.c b/tests/fixtures/integration.c index a3b1c7e25d..0e79f25f0d 100644 --- a/tests/fixtures/integration.c +++ b/tests/fixtures/integration.c @@ -1320,6 +1320,12 @@ main(int argc, char **argv) sentry_scope_add_attachment(scope, bytes); } + if (has_arg(argc, argv, "set-scope-trace")) { + const char *scope_trace_id = "11112222333344445555666677778888"; + const char *scope_parent_span_id = "0f0f0f0f0f0f0f0f"; + sentry_scope_set_trace(scope, scope_trace_id, scope_parent_span_id); + } + sentry_scope_capture_event(scope, event, NULL); } diff --git a/tests/test_integration_transactions.py b/tests/test_integration_transactions.py index 85af227922..e121a4722f 100644 --- a/tests/test_integration_transactions.py +++ b/tests/test_integration_transactions.py @@ -343,6 +343,50 @@ def test_set_trace_event(cmake, httpserver): assert trace_context["parent_span_id"] == "f0f0f0f0f0f0f0f0" +def test_set_scope_trace_event(cmake, httpserver): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver), SENTRY_RELEASE="🤮🚀") + + run( + tmp_path, + "sentry_test_integration", + ["log", "set-trace", "set-scope-trace", "capture-with-scope"], + env=env, + ) + + assert len(httpserver.log) == 1 + event_req = httpserver.log[0][0] + event_body = event_req.get_data() + + event_envelope = Envelope.deserialize(event_body) + + # Extract the one-and-only-item + (event,) = event_envelope.items + + payload = event.payload.json + + # the local scope's trace takes precedence over the isolation scope's + trace_context = payload["contexts"]["trace"] + assert trace_context["trace_id"] == "11112222333344445555666677778888" + assert trace_context["parent_span_id"] == "0f0f0f0f0f0f0f0f" + + trace_header = event_envelope.headers["trace"] + assert trace_header["sample_rand"] == trace_context["sample_rand"] + del trace_header["sample_rand"] + assert trace_header == { + "environment": "development", + "public_key": "uiaeosnrtdy", + "release": "test-example-release", + "sample_rate": 0, + "sampled": "false", + "trace_id": "11112222333344445555666677778888", + } + + assert "dsc" not in payload + + def test_set_trace_transaction_scoped_event(cmake, httpserver): tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index 8475754a23..6c2ed95b5a 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -21,10 +21,7 @@ static char *const SERIALIZED_ENVELOPE_STR = "{\"dsn\":\"https://foo@sentry.invalid/42\"," - "\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"trace\":{" - "\"public_key\":\"foo\",\"sample_rate\":0,\"sample_" - "rand\":0.01006918276309107,\"release\":\"test-release\",\"environment\":" - "\"production\",\"sampled\":\"false\"}}\n" + "\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\"}\n" "{\"type\":\"event\",\"length\":71}\n" "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"some-" "context\":1234}\n" diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 14417092e8..3da72ebd0f 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -463,6 +463,120 @@ SENTRY_TEST(scope_propagation_context) sentry_close(); } +SENTRY_TEST(scope_set_trace) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + sentry_value_t global_trace = sentry_value_new_null(); + SENTRY_WITH_SCOPE (global_scope) { + global_trace = sentry__scope_load_trace_context(global_scope); + } + + sentry_scope_t *local_scope = sentry_local_scope_new(); + TEST_CHECK(sentry__scope_is_trace_managed(local_scope)); + + sentry_scope_set_trace( + local_scope, "aaaabbbbccccddddeeeeffff00001111", "9c2a92b49b7c4d1e"); + TEST_CHECK(!sentry__scope_is_trace_managed(local_scope)); + + 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_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + trace, "parent_span_id")), + "9c2a92b49b7c4d1e"); + 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); + + // the global scope keeps its trace + SENTRY_WITH_SCOPE (global_scope) { + trace = sentry__scope_load_trace_context(global_scope); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(trace, "trace_id")), + sentry_value_as_string( + sentry_value_get_by_key(global_trace, "trace_id"))); + sentry_value_decref(trace); + } + sentry_value_decref(global_trace); + + sentry_scope_set_trace_n( + local_scope, "11112222333344445555666677778888xxxx", 32, NULL, 0); + trace = sentry__scope_load_trace_context(local_scope); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(trace, "trace_id")), + "11112222333344445555666677778888"); + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(trace, "parent_span_id"))); + sentry_value_decref(trace); + + sentry_scope_free(local_scope); + sentry_close(); +} + +static void +store_trace_header(sentry_envelope_t *envelope, void *data) +{ + sentry_value_t *trace = data; + sentry_value_decref(*trace); + *trace = sentry_value_incref(sentry_envelope_get_header(envelope, "trace")); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(sentry_envelope_get_event(envelope), "dsc"))); + sentry_envelope_free(envelope); +} + +SENTRY_TEST(scope_capture_dsc) +{ + sentry_value_t trace = sentry_value_new_null(); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_traces_sample_rate(options, 0.5); + sentry_transport_t *transport = sentry_transport_new(store_trace_header); + sentry_transport_set_state(transport, &trace); + sentry_options_set_transport(options, transport); + sentry_init(options); + + SENTRY_WITH_SCOPE_MUT (global_scope) { + sentry__scope_set_trace_context( + global_scope, "sample_rand", sentry_value_new_double(0.25)); + } + + sentry_scope_t *scope = sentry_scope_new(); + sentry_scope_set_trace( + scope, "aaaabbbbccccddddeeeeffff00001111", "9c2a92b49b7c4d1e"); + sentry__scope_set_trace_context( + scope, "sample_rand", sentry_value_new_double(0.75)); + + sentry_scope_capture_event(scope, sentry_value_new_event(), NULL); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(trace, "trace_id")), + "aaaabbbbccccddddeeeeffff00001111"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(trace, "sampled")), + "false"); + + sentry_value_decref(trace); + trace = sentry_value_new_null(); + sentry_scope_capture_feedback( + scope, sentry_value_new_feedback("feedback", NULL, NULL, NULL), NULL); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(trace, "trace_id")), + "aaaabbbbccccddddeeeeffff00001111"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(trace, "sampled")), + "false"); + + sentry_scope_free(scope); + sentry_close(); + sentry_value_decref(trace); +} + SENTRY_TEST(scope_extra) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index fb231a5125..5826f9bee6 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -411,6 +411,7 @@ XX(scope_bind_span) XX(scope_bind_span_or_transaction_not_both) XX(scope_bind_transaction_object) XX(scope_breadcrumbs) +XX(scope_capture_dsc) XX(scope_capture_log_attributes) XX(scope_capture_log_one_shot) XX(scope_capture_log_trace_id) @@ -473,6 +474,7 @@ 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_set_trace) XX(scope_tags) XX(scope_tags_flush) XX(scope_transaction) From d9c645ce6030f8d74d5bf5378293e9e7a82c0aa2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 26 Oct 2026 13:07:17 +0100 Subject: [PATCH 30/47] feat: add `sentry_continue_trace` (#2237) * feat: add `sentry_continue_trace` Add `sentry_continue_trace()`, `sentry_scope_continue_trace()` and their `_n` variants to continue a trace from incoming `sentry-trace` and `baggage` headers without starting a transaction, as required by the trace propagation spec's `continueTrace`. The headers are parsed with the same code as `sentry_transaction_context_update_from_header()`. When strict trace continuation allows it, the scope's propagation context takes the incoming trace and parent span IDs, sampling decision and `sample_rand`, and the incoming baggage is frozen as the DSC. Otherwise, a new trace is started. Like with `sentry_set_trace()`, transactions no longer act as trace boundaries afterwards. Per the spec, a missing `sentry-sample_rand` is derived from the incoming `sample_rate` and sampling decision, transactions started afterwards inherit the sampling decision, and the DSC of error events reports it instead of recomputing `sampled` from the local sample rate. * Update CHANGELOG.md * fix: set/continue traces from the initial scope `sentry_scope_continue_trace()` looked up the global options, which are not installed yet while `sentry_init()` runs the initial scope callback, so a trace continued from there was dropped. Take the options from the scope chain instead, where the isolation scope carries them already, and do the same in `sentry_scope_set_trace()`. Also build the default DSC before running the initial scope callback, so that it no longer overwrites a DSC frozen from incoming baggage. * fix: continue traces on cloned scopes A clone of the isolation scope has neither its own options nor a parent that carries them, so `ref_options()` found none, which made `sentry_scope_continue_trace()` a no-op on it and left the DSC stale in `sentry_scope_set_trace()`. Fall back to the global options when the scope chain carries none. --- CHANGELOG.md | 1 + include/sentry.h | 21 +++ src/sentry_core.c | 17 ++ src/sentry_scope.c | 76 ++++++++- src/sentry_tracing.c | 72 +++++++-- src/sentry_tracing.h | 9 ++ tests/fixtures/integration.c | 7 + tests/test_integration_transactions.py | 38 +++++ tests/unit/test_scope.c | 42 +++++ tests/unit/test_tracing.c | 212 +++++++++++++++++++++++++ tests/unit/tests.inc | 8 + 11 files changed, 491 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4dd69d63..b847903448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - 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)) - Add `sentry_scope_set_trace` for per-scope traces. ([#2236](https://github.com/getsentry/sentry-native/pull/2236)) +- Add `sentry_continue_trace` to continue incoming traces without a transaction. ([#2237](https://github.com/getsentry/sentry-native/pull/2237)) **Fixes**: diff --git a/include/sentry.h b/include/sentry.h index f0bf7b3f2f..1ddbb44b06 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -3002,6 +3002,27 @@ SENTRY_API void sentry_scope_set_trace_n(sentry_scope_t *scope, const char *trace_id, size_t trace_id_len, const char *parent_span_id, size_t parent_span_id_len); +/** + * Continues a trace from the `sentry-trace` and `baggage` headers of an + * incoming request, without starting a transaction. Either header may be NULL. + * + * Events and transactions afterwards continue the incoming trace and inherit + * its sampling decision. If the headers carry no valid trace, or strict trace + * continuation rejects it, a new trace is started instead. + * + * As with `sentry_set_trace`, transactions no longer act as automatic trace + * boundaries. + */ +SENTRY_API void sentry_continue_trace( + const char *sentry_trace, const char *baggage); +SENTRY_API void sentry_continue_trace_n(const char *sentry_trace, + size_t sentry_trace_len, const char *baggage, size_t baggage_len); +SENTRY_API void sentry_scope_continue_trace( + sentry_scope_t *scope, const char *sentry_trace, const char *baggage); +SENTRY_API void sentry_scope_continue_trace_n(sentry_scope_t *scope, + const char *sentry_trace, size_t sentry_trace_len, const char *baggage, + size_t baggage_len); + /** * Generates a new random `trace_id` and `span_id` and sets these onto * the propagation context. Use this to set a trace boundary for diff --git a/src/sentry_core.c b/src/sentry_core.c index c0bd374bab..344412a2cb 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1471,6 +1471,23 @@ sentry_set_trace_n(const char *trace_id, size_t trace_id_len, } } +void +sentry_continue_trace(const char *sentry_trace, const char *baggage) +{ + sentry_continue_trace_n(sentry_trace, sentry__guarded_strlen(sentry_trace), + baggage, sentry__guarded_strlen(baggage)); +} + +void +sentry_continue_trace_n(const char *sentry_trace, size_t sentry_trace_len, + const char *baggage, size_t baggage_len) +{ + SENTRY_WITH_SCOPE_MUT (scope) { + sentry_scope_continue_trace_n( + scope, sentry_trace, sentry_trace_len, baggage, baggage_len); + } +} + void sentry_start_new_trace(void) { diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 29afd2b7dd..643050bafc 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -1052,11 +1052,11 @@ sentry__scope_apply_options(sentry_scope_t *scope, sentry_options_t *options) sentry__ringbuffer_set_max_size( data->breadcrumbs, options->max_breadcrumbs); } + sentry__scope_update_dsc(scope, options); if (options->initial_scope_func) { options->initial_scope_func(scope, options->initial_scope_data); } - sentry__scope_update_dsc(scope, options); } void @@ -1286,9 +1286,16 @@ make_dsc(const sentry_scope_t *scope, const sentry_options_t *options) sentry_value_t trace_context = sentry__scope_load_trace_context(scope); double sample_rand = sentry_value_as_double( sentry_value_get_by_key(trace_context, "sample_rand")); + sentry_value_t sampled + = sentry_value_get_by_key_owned(trace_context, "sampled"); sentry_value_decref(trace_context); if (!sentry_value_is_null(dsc)) { - if (!isnan(sample_rand)) { + if (!sentry_value_is_null(sampled)) { + // inherit the incoming sampling decision + sentry_value_set_by_key(dsc, "sampled", + sentry_value_new_string( + sentry_value_is_true(sampled) ? "true" : "false")); + } else if (!isnan(sample_rand)) { if (sample_rand >= options->traces_sample_rate) { sentry_value_set_by_key( dsc, "sampled", sentry_value_new_string("false")); @@ -1303,6 +1310,7 @@ make_dsc(const sentry_scope_t *scope, const sentry_options_t *options) "the dynamic sampling context"); } } + sentry_value_decref(sampled); return dsc; } @@ -2360,6 +2368,20 @@ sentry_scope_remove_fingerprint(sentry_scope_t *scope) sentry__scope_flush(scope); } +// the options attached to the isolation scope, available already while +// `sentry_init` applies the initial scope, or the global options +static sentry_options_t * +ref_options(const sentry_scope_t *scope) +{ + sentry_options_t *options = NULL; + for (; scope && !options; scope = scope->parent) { + sentry__mutex_lock((sentry_mutex_t *)&scope->observers_lock); + options = sentry__options_incref(scope->state); + sentry__mutex_unlock((sentry_mutex_t *)&scope->observers_lock); + } + return options ? options : (sentry_options_t *)sentry__options_getref(); +} + void sentry_scope_set_trace( sentry_scope_t *scope, const char *trace_id, const char *parent_span_id) @@ -2392,10 +2414,58 @@ sentry_scope_set_trace_n(sentry_scope_t *scope, const char *trace_id, sentry__scope_set_propagation_context(scope, "trace", context); - SENTRY_WITH_OPTIONS (options) { + sentry_options_t *options = ref_options(scope); + if (options) { sentry__scope_update_dsc(scope, options); sentry__scope_flush(scope); } + sentry_options_free(options); +} + +void +sentry_scope_continue_trace( + sentry_scope_t *scope, const char *sentry_trace, const char *baggage) +{ + sentry_scope_continue_trace_n(scope, sentry_trace, + sentry__guarded_strlen(sentry_trace), baggage, + sentry__guarded_strlen(baggage)); +} + +void +sentry_scope_continue_trace_n(sentry_scope_t *scope, const char *sentry_trace, + size_t sentry_trace_len, const char *baggage, size_t baggage_len) +{ + sentry_value_t context = sentry__parse_trace_headers( + sentry_trace, sentry_trace_len, baggage, baggage_len); + sentry_value_t dsc = sentry__value_take_by_key(context, "incoming_dsc"); + + sentry_options_t *options = ref_options(scope); + if (options) { + if (!sentry_value_is_null(sentry_value_get_by_key(context, "trace_id")) + && sentry__trace_can_continue(dsc, options)) { + sentry_value_set_by_key( + context, "type", sentry_value_new_string("trace")); + sentry_uuid_t span_id = sentry_uuid_new_v4(); + sentry_value_set_by_key( + context, "span_id", sentry__value_new_span_uuid(&span_id)); + sentry__scope_set_propagation_context( + scope, "trace", sentry_value_incref(context)); + if (sentry_value_get_length(dsc) > 0) { + sentry__scope_freeze_dsc(scope, dsc); + } else { + sentry__scope_update_dsc(scope, options); + } + } else { + // start a new trace if the incoming one cannot be continued + sentry__scope_regenerate_propagation_context(scope); + sentry__scope_update_dsc(scope, options); + } + sentry__scope_set_trace_managed(scope, false); + sentry__scope_flush(scope); + } + sentry_options_free(options); + sentry_value_decref(dsc); + sentry_value_decref(context); } sentry_level_t diff --git a/src/sentry_tracing.c b/src/sentry_tracing.c index ec0967aef5..67c766f629 100644 --- a/src/sentry_tracing.c +++ b/src/sentry_tracing.c @@ -10,6 +10,7 @@ #include "sentry_utils.h" #include "sentry_value.h" #include +#include #include static inline bool @@ -109,6 +110,12 @@ transaction_context_new_n(sentry_slice_t name, sentry_slice_t operation) sentry_value_set_by_key(transaction_context, "parent_span_id", sentry__value_clone( sentry_value_get_by_key(trace_context, "parent_span_id"))); + sentry_value_t sampled + = sentry_value_get_by_key(trace_context, "sampled"); + if (!sentry_value_is_null(sampled)) { + sentry_value_set_by_key(transaction_context, "sampled", + sentry__value_clone(sampled)); + } } sentry_value_decref(trace_context); } @@ -287,8 +294,7 @@ compare_header_key( } static void -parse_sentry_trace( - sentry_transaction_context_t *tx_ctx, const char *value, size_t value_len) +parse_sentry_trace(sentry_value_t inner, const char *value, size_t value_len) { // Parse sentry-trace header: traceid-spanid(-sampled)? const char *trace_id_start = value; @@ -298,8 +304,6 @@ parse_sentry_trace( return; } - sentry_value_t inner = tx_ctx->inner; - char *s = sentry__string_clone_n( trace_id_start, (size_t)(trace_id_end - trace_id_start)); if (!is_valid_trace_id(s)) { @@ -350,15 +354,13 @@ parse_sentry_trace( } static void -parse_baggage( - sentry_transaction_context_t *tx_ctx, const char *value, size_t value_len) +parse_baggage(sentry_value_t inner, const char *value, size_t value_len) { // https://www.w3.org/TR/baggage/ — Sentry-prefixed members are kept and // percent-decoded; non-sentry members are ignored. static const char sentry_prefix[] = "sentry-"; static const size_t sentry_prefix_len = sizeof(sentry_prefix) - 1; - sentry_value_t inner = tx_ctx->inner; sentry_value_t incoming = sentry_value_get_by_key(inner, "incoming_dsc"); if (sentry_value_is_null(incoming)) { incoming = sentry_value_new_object(); @@ -405,6 +407,58 @@ sentry__trace_can_continue( return true; } +static double +parse_double(sentry_value_t value) +{ + const char *str = sentry_value_as_string(value); + char *end = NULL; + double d = sentry__strtod_c(str, &end); + return end != str && *end == '\0' ? d : (double)NAN; +} + +sentry_value_t +sentry__parse_trace_headers(const char *sentry_trace, size_t sentry_trace_len, + const char *baggage, size_t baggage_len) +{ + sentry_value_t trace = sentry_value_new_object(); + if (sentry_trace && sentry_trace_len) { + parse_sentry_trace(trace, sentry_trace, sentry_trace_len); + } + if (baggage && baggage_len) { + parse_baggage(trace, baggage, baggage_len); + } + + sentry_value_t dsc = sentry_value_get_by_key(trace, "incoming_dsc"); + double sample_rand + = parse_double(sentry_value_get_by_key(dsc, "sample_rand")); + if (sample_rand >= 0.0 && sample_rand < 1.0) { + sentry_value_set_by_key( + trace, "sample_rand", sentry_value_new_double(sample_rand)); + return trace; + } + + sentry__generate_sample_rand(trace); + sentry_value_t sampled = sentry_value_get_by_key(trace, "sampled"); + double sample_rate + = parse_double(sentry_value_get_by_key(dsc, "sample_rate")); + if (!sentry_value_is_null(sampled) && sample_rate >= 0.0 + && sample_rate <= 1.0) { + // keep the incoming sampling decision when compared to `sample_rate` + sample_rand = sentry_value_as_double( + sentry_value_get_by_key(trace, "sample_rand")); + sample_rand = sentry_value_is_true(sampled) + ? sample_rand * sample_rate + : sample_rate + sample_rand * (1.0 - sample_rate); + sentry_value_set_by_key( + trace, "sample_rand", sentry_value_new_double(sample_rand)); + } + if (sentry_value_get_length(dsc) > 0) { + sentry_value_set_by_key(dsc, "sample_rand", + sentry_value_incref(sentry_value_get_by_key(trace, "sample_rand"))); + } + return trace; +} + void sentry_transaction_context_update_from_header_n( sentry_transaction_context_t *tx_ctx, const char *key, size_t key_len, @@ -418,14 +472,14 @@ sentry_transaction_context_update_from_header_n( const char sentry_trace[] = "sentry-trace"; const size_t sentry_trace_len = sizeof(sentry_trace) - 1; if (compare_header_key(key, key_len, sentry_trace, sentry_trace_len)) { - parse_sentry_trace(tx_ctx, value, value_len); + parse_sentry_trace(tx_ctx->inner, value, value_len); return; } const char baggage[] = "baggage"; const size_t baggage_len = sizeof(baggage) - 1; if (compare_header_key(key, key_len, baggage, baggage_len)) { - parse_baggage(tx_ctx, value, value_len); + parse_baggage(tx_ctx->inner, value, value_len); return; } } diff --git a/src/sentry_tracing.h b/src/sentry_tracing.h index 2c9907794a..5c259eeb92 100644 --- a/src/sentry_tracing.h +++ b/src/sentry_tracing.h @@ -93,4 +93,13 @@ sentry_value_t sentry__value_get_trace_context(sentry_value_t span); bool sentry__trace_can_continue( sentry_value_t incoming, const sentry_options_t *options); +/** + * Parses incoming `sentry-trace` and `baggage` headers into an object with + * `trace_id`, `parent_span_id`, `sampled`, `sample_rand` and `incoming_dsc`. + * Missing or invalid values are left out, except `sample_rand`, which is + * derived from the incoming sampling decision or generated when missing. + */ +sentry_value_t sentry__parse_trace_headers(const char *sentry_trace, + size_t sentry_trace_len, const char *baggage, size_t baggage_len); + #endif diff --git a/tests/fixtures/integration.c b/tests/fixtures/integration.c index 0e79f25f0d..857c20db7b 100644 --- a/tests/fixtures/integration.c +++ b/tests/fixtures/integration.c @@ -1233,6 +1233,13 @@ main(int argc, char **argv) sentry_set_trace(direct_trace_id, direct_parent_span_id); } + if (has_arg(argc, argv, "continue-trace")) { + sentry_continue_trace( + "aaaabbbbccccddddeeeeffff00001111-f0f0f0f0f0f0f0f0-1", + "sentry-trace_id=aaaabbbbccccddddeeeeffff00001111," + "sentry-environment=upstream,sentry-sample_rand=0.25"); + } + if (has_arg(argc, argv, "replay-context")) { // mimics an embedder (e.g. sentry-unreal) that stages replay clips in // `/replays/` and announces the active replay on the scope diff --git a/tests/test_integration_transactions.py b/tests/test_integration_transactions.py index e121a4722f..3c85ad6af9 100644 --- a/tests/test_integration_transactions.py +++ b/tests/test_integration_transactions.py @@ -387,6 +387,44 @@ def test_set_scope_trace_event(cmake, httpserver): assert "dsc" not in payload +def test_continue_trace_event(cmake, httpserver): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver), SENTRY_RELEASE="🤮🚀") + + run( + tmp_path, + "sentry_test_integration", + ["log", "continue-trace", "capture-event"], + env=env, + ) + + assert len(httpserver.log) == 1 + event_req = httpserver.log[0][0] + event_body = event_req.get_data() + + event_envelope = Envelope.deserialize(event_body) + + # Extract the one-and-only-item + (event,) = event_envelope.items + + payload = event.payload.json + + trace_context = payload["contexts"]["trace"] + assert trace_context["trace_id"] == "aaaabbbbccccddddeeeeffff00001111" + assert trace_context["parent_span_id"] == "f0f0f0f0f0f0f0f0" + assert trace_context["sampled"] is True + + # the frozen upstream DSC with the inherited sampling decision + assert event_envelope.headers["trace"] == { + "environment": "upstream", + "sample_rand": "0.25", + "sampled": "true", + "trace_id": "aaaabbbbccccddddeeeeffff00001111", + } + + def test_set_trace_transaction_scoped_event(cmake, httpserver): tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 3da72ebd0f..15948b8c7e 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -577,6 +577,48 @@ SENTRY_TEST(scope_capture_dsc) sentry_value_decref(trace); } +SENTRY_TEST(scope_continue_trace) +{ + sentry_value_t trace = sentry_value_new_null(); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_transport_t *transport = sentry_transport_new(store_trace_header); + sentry_transport_set_state(transport, &trace); + sentry_options_set_transport(options, transport); + sentry_init(options); + + sentry_scope_t *scope = sentry_scope_new(); + sentry_scope_continue_trace(scope, + "aaaabbbbccccddddeeeeffff00001111-9c2a92b49b7c4d1e-1", + "sentry-environment=upstream"); + + // the global scope keeps its trace + SENTRY_WITH_SCOPE (global_scope) { + sentry_value_t global_trace + = sentry__scope_load_trace_context(global_scope); + TEST_CHECK(strcmp(sentry_value_as_string(sentry_value_get_by_key( + global_trace, "trace_id")), + "aaaabbbbccccddddeeeeffff00001111") + != 0); + sentry_value_decref(global_trace); + } + + sentry_scope_capture_event(scope, sentry_value_new_event(), NULL); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(trace, "trace_id")), + "aaaabbbbccccddddeeeeffff00001111"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(trace, "environment")), + "upstream"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(trace, "sampled")), + "true"); + + sentry_scope_free(scope); + sentry_close(); + sentry_value_decref(trace); +} + SENTRY_TEST(scope_extra) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/test_tracing.c b/tests/unit/test_tracing.c index a3447689ef..f86a0b5c01 100644 --- a/tests/unit/test_tracing.c +++ b/tests/unit/test_tracing.c @@ -2597,6 +2597,218 @@ SENTRY_TEST(set_trace_rebuilds_dsc_sample_rand) sentry_close(); } +SENTRY_TEST(continue_trace) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_transport( + options, sentry_transport_new(discard_envelope)); + // the upstream sampling decision wins over the local rate + sentry_options_set_traces_sample_rate(options, 0.0); + sentry_init(options); + + sentry_continue_trace(UPSTREAM_SENTRY_TRACE, + "sentry-org_id=123456,sentry-environment=upstream," + "sentry-sample_rand=0.25"); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t trace = sentry__scope_load_trace_context(scope); + CHECK_STRING_PROPERTY(trace, "trace_id", UPSTREAM_TRACE_ID); + CHECK_STRING_PROPERTY(trace, "parent_span_id", UPSTREAM_PARENT_SPAN_ID); + TEST_CHECK( + sentry_value_is_true(sentry_value_get_by_key(trace, "sampled"))); + TEST_CHECK(sentry_value_as_double( + sentry_value_get_by_key(trace, "sample_rand")) + == 0.25); + sentry_value_decref(trace); + + sentry_value_t dsc = sentry__scope_load_dsc(scope); + CHECK_STRING_PROPERTY(dsc, "org_id", "123456"); + CHECK_STRING_PROPERTY(dsc, "environment", "upstream"); + CHECK_STRING_PROPERTY(dsc, "sample_rand", "0.25"); + sentry_value_decref(dsc); + } + + sentry_transaction_t *tx = sentry_transaction_start( + sentry_transaction_context_new("name", "op"), sentry_value_new_null()); + CHECK_STRING_PROPERTY(tx->inner, "trace_id", UPSTREAM_TRACE_ID); + CHECK_STRING_PROPERTY(tx->inner, "parent_span_id", UPSTREAM_PARENT_SPAN_ID); + TEST_CHECK( + sentry_value_is_true(sentry_value_get_by_key(tx->inner, "sampled"))); + + sentry_transaction_finish(tx); + sentry_close(); +} + +SENTRY_TEST(continue_trace_org_mismatch_starts_new_trace) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://k@o123456.ingest.sentry.io/1"); + sentry_init(options); + + sentry_continue_trace(UPSTREAM_SENTRY_TRACE, "sentry-org_id=99999"); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t trace = sentry__scope_load_trace_context(scope); + TEST_CHECK(strcmp(sentry_value_as_string( + sentry_value_get_by_key(trace, "trace_id")), + UPSTREAM_TRACE_ID) + != 0); + TEST_CHECK(IS_NULL(trace, "parent_span_id")); + TEST_CHECK(IS_NULL(trace, "sampled")); + sentry_value_decref(trace); + + sentry_value_t dsc = sentry__scope_load_dsc(scope); + CHECK_STRING_PROPERTY(dsc, "org_id", "123456"); + sentry_value_decref(dsc); + } + + sentry_close(); +} + +SENTRY_TEST(continue_trace_without_trace_starts_new_trace) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + const char *headers[] = { NULL, "invalid" }; + for (size_t i = 0; i < 2; i++) { + char *trace_id = NULL; + SENTRY_WITH_SCOPE (scope) { + sentry_value_t trace = sentry__scope_load_trace_context(scope); + trace_id = sentry__string_clone(sentry_value_as_string( + sentry_value_get_by_key(trace, "trace_id"))); + sentry_value_decref(trace); + } + + sentry_continue_trace(headers[i], NULL); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t trace = sentry__scope_load_trace_context(scope); + TEST_CHECK(strcmp(sentry_value_as_string( + sentry_value_get_by_key(trace, "trace_id")), + trace_id) + != 0); + TEST_CHECK(IS_NULL(trace, "parent_span_id")); + sentry_value_decref(trace); + } + sentry_free(trace_id); + } + + sentry_close(); +} + +static double +load_sample_rand(void) +{ + double sample_rand = 1.0; + SENTRY_WITH_SCOPE (scope) { + sentry_value_t trace = sentry__scope_load_trace_context(scope); + sample_rand = sentry_value_as_double( + sentry_value_get_by_key(trace, "sample_rand")); + sentry_value_decref(trace); + + // the derived value propagates downstream + sentry_value_t dsc = sentry__scope_load_dsc(scope); + TEST_CHECK( + sentry_value_as_double(sentry_value_get_by_key(dsc, "sample_rand")) + == sample_rand); + sentry_value_decref(dsc); + } + return sample_rand; +} + +SENTRY_TEST(continue_trace_derives_sample_rand) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + sentry_continue_trace(UPSTREAM_TRACE_ID "-" UPSTREAM_PARENT_SPAN_ID "-1", + "sentry-sample_rate=0.25"); + double sample_rand = load_sample_rand(); + TEST_CHECK(sample_rand >= 0.0 && sample_rand < 0.25); + + sentry_continue_trace(UPSTREAM_TRACE_ID "-" UPSTREAM_PARENT_SPAN_ID "-0", + "sentry-sample_rate=0.25"); + sample_rand = load_sample_rand(); + TEST_CHECK(sample_rand >= 0.25 && sample_rand < 1.0); + + sentry_close(); +} + +static void +continue_upstream_trace(sentry_scope_t *scope, void *UNUSED(data)) +{ + sentry_scope_continue_trace( + scope, UPSTREAM_SENTRY_TRACE, "sentry-environment=upstream"); +} + +SENTRY_TEST(continue_trace_on_init) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_initial_scope(options, continue_upstream_trace, NULL); + sentry_init(options); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t trace = sentry__scope_load_trace_context(scope); + CHECK_STRING_PROPERTY(trace, "trace_id", UPSTREAM_TRACE_ID); + sentry_value_decref(trace); + + sentry_value_t dsc = sentry__scope_load_dsc(scope); + CHECK_STRING_PROPERTY(dsc, "environment", "upstream"); + sentry_value_decref(dsc); + } + + sentry_close(); +} + +static void +set_upstream_trace(sentry_scope_t *scope, void *UNUSED(data)) +{ + sentry_scope_set_trace(scope, UPSTREAM_TRACE_ID, UPSTREAM_PARENT_SPAN_ID); +} + +SENTRY_TEST(set_trace_on_init) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_initial_scope(options, set_upstream_trace, NULL); + sentry_init(options); + + SENTRY_WITH_SCOPE (scope) { + sentry_value_t trace = sentry__scope_load_trace_context(scope); + CHECK_STRING_PROPERTY(trace, "trace_id", UPSTREAM_TRACE_ID); + + sentry_value_t dsc = sentry__scope_load_dsc(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_close(); +} + +SENTRY_TEST(continue_trace_on_clone) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + sentry_scope_t *clone = NULL; + SENTRY_WITH_SCOPE (scope) { + clone = sentry_scope_clone(scope); + } + sentry_scope_continue_trace(clone, UPSTREAM_SENTRY_TRACE, NULL); + + sentry_value_t trace = sentry__scope_load_trace_context(clone); + CHECK_STRING_PROPERTY(trace, "trace_id", UPSTREAM_TRACE_ID); + sentry_value_decref(trace); + + sentry_scope_free(clone); + sentry_close(); +} + #undef UPSTREAM_SENTRY_TRACE #undef UPSTREAM_PARENT_SPAN_ID #undef UPSTREAM_TRACE_ID diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 5826f9bee6..d52d208720 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -134,6 +134,12 @@ XX(concurrent_uninit) XX(cond_wait_timeout_overflow) XX(cond_wake_all) XX(continuation_no_baggage_uses_sdk_dsc) +XX(continue_trace) +XX(continue_trace_derives_sample_rand) +XX(continue_trace_on_clone) +XX(continue_trace_on_init) +XX(continue_trace_org_mismatch_starts_new_trace) +XX(continue_trace_without_trace_starts_new_trace) XX(corrupt_crash_envelope_does_not_block_old_run) XX(count_sampled_events) XX(crash_context_handler_path_propagation) @@ -429,6 +435,7 @@ XX(scope_clone_keeps_bound_span) XX(scope_clone_preserves_data) XX(scope_clone_shares_span) XX(scope_contexts) +XX(scope_continue_trace) XX(scope_defaults) XX(scope_environment) XX(scope_extra) @@ -495,6 +502,7 @@ XX(set_trace) XX(set_trace_id_before_scoped_txn) XX(set_trace_id_twice) XX(set_trace_id_with_txn) +XX(set_trace_on_init) XX(set_trace_rebuilds_dsc_sample_rand) XX(set_trace_update_from_header) XX(set_trace_without_parent) From 3e1a9c8263c2a1cc0546f3965a864ecfa30d3e78 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 26 Oct 2026 13:07:18 +0100 Subject: [PATCH 31/47] feat: add `sentry_iter_trace_propagation_headers` (#2240) * feat: add `sentry_iter_trace_propagation_headers` Add `sentry_iter_trace_propagation_headers()` and `sentry_scope_iter_trace_propagation_headers()` to fetch the `sentry-trace` and `baggage` headers for downstream services without an active span, as required by the trace propagation spec's `getTraceData`. Until now, only `sentry_span_iter_headers()` and `sentry_transaction_iter_headers()` existed, so a trace could not be propagated without tracing. The headers come from the span or transaction bound to the scope, if any, otherwise from the scope's propagation context. Without a span, the sampling decision is deferred unless one was inherited with `sentry_continue_trace()`. Per the spec, the `sentry-trace` header then has no sampled flag and the SDK-built baggage has no `sentry-sampled`. The scoped variant falls back to the parent scopes for what the scope does not set. Ref: #2050 * Update CHANGELOG.md --- CHANGELOG.md | 1 + include/sentry.h | 14 +++++++ src/sentry_core.c | 9 +++++ src/sentry_scope.c | 12 ++++++ src/sentry_tracing.c | 28 ++++++++----- src/sentry_tracing.h | 8 ++++ tests/fixtures/integration.c | 11 ++++++ tests/test_integration_transactions.py | 40 +++++++++++++++++++ tests/unit/test_scope.c | 33 ++++++++++++++++ tests/unit/test_tracing.c | 54 ++++++++++++++++++++++++++ tests/unit/tests.inc | 2 + 11 files changed, 202 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b847903448..8620e2d552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - 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)) - Add `sentry_scope_set_trace` for per-scope traces. ([#2236](https://github.com/getsentry/sentry-native/pull/2236)) - Add `sentry_continue_trace` to continue incoming traces without a transaction. ([#2237](https://github.com/getsentry/sentry-native/pull/2237)) +- Add `sentry_iter_trace_propagation_headers` to propagate traces without a span. ([#2240](https://github.com/getsentry/sentry-native/pull/2240)) **Fixes**: diff --git a/include/sentry.h b/include/sentry.h index 1ddbb44b06..258197a680 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -4486,6 +4486,20 @@ SENTRY_API void sentry_span_iter_headers(sentry_span_t *span, SENTRY_API void sentry_transaction_iter_headers(sentry_transaction_t *tx, sentry_iter_headers_function_t callback, void *userdata); +/** + * Iterates the distributed tracing HTTP headers for the current trace: of the + * span or transaction bound to the scope, if any, otherwise of the scope's + * propagation context. This allows propagating a trace without a span. + * + * Without a span, the `sentry-trace` header carries a sampling decision only + * if one was inherited with `sentry_continue_trace`. + */ +SENTRY_API void sentry_iter_trace_propagation_headers( + sentry_iter_headers_function_t callback, void *userdata); +SENTRY_API void sentry_scope_iter_trace_propagation_headers( + const sentry_scope_t *scope, sentry_iter_headers_function_t callback, + void *userdata); + /** * Returns whether the application has crashed on the last run. * diff --git a/src/sentry_core.c b/src/sentry_core.c index 344412a2cb..a082addf4f 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1488,6 +1488,15 @@ sentry_continue_trace_n(const char *sentry_trace, size_t sentry_trace_len, } } +void +sentry_iter_trace_propagation_headers( + sentry_iter_headers_function_t callback, void *userdata) +{ + SENTRY_WITH_SCOPE (scope) { + sentry_scope_iter_trace_propagation_headers(scope, callback, userdata); + } +} + void sentry_start_new_trace(void) { diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 643050bafc..15243205d7 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -2468,6 +2468,18 @@ sentry_scope_continue_trace_n(sentry_scope_t *scope, const char *sentry_trace, sentry_value_decref(context); } +void +sentry_scope_iter_trace_propagation_headers(const sentry_scope_t *scope, + sentry_iter_headers_function_t callback, void *userdata) +{ + sentry_value_t span = sentry__scope_load_span_or_transaction(scope); + if (sentry_value_is_null(span)) { + span = sentry__scope_load_trace_context(scope); + } + sentry__span_iter_headers(scope, span, callback, userdata); + sentry_value_decref(span); +} + sentry_level_t sentry__scope_get_level(const sentry_scope_t *scope) { diff --git a/src/sentry_tracing.c b/src/sentry_tracing.c index 67c766f629..2a0b350a63 100644 --- a/src/sentry_tracing.c +++ b/src/sentry_tracing.c @@ -985,8 +985,8 @@ sentry_transaction_set_status( } } -static void -sentry__span_iter_headers(sentry_value_t span, +void +sentry__span_iter_headers(const sentry_scope_t *scope, sentry_value_t span, sentry_iter_headers_function_t callback, void *userdata) { sentry_value_t trace_id = sentry_value_get_by_key(span, "trace_id"); @@ -999,9 +999,15 @@ sentry__span_iter_headers(sentry_value_t span, // (32 char trace_id)-(16-char span_id)-(0|1) + null terminator char buf[SENTRY_TRACE_LEN + 1]; - snprintf(buf, sizeof(buf), "%s-%s-%s", sentry_value_as_string(trace_id), - sentry_value_as_string(span_id), - sentry_value_is_true(sampled) ? "1" : "0"); + if (sentry_value_is_null(sampled)) { + // deferred sampling decision + snprintf(buf, sizeof(buf), "%s-%s", sentry_value_as_string(trace_id), + sentry_value_as_string(span_id)); + } else { + snprintf(buf, sizeof(buf), "%s-%s-%s", sentry_value_as_string(trace_id), + sentry_value_as_string(span_id), + sentry_value_is_true(sampled) ? "1" : "0"); + } callback("sentry-trace", buf, userdata); // Outgoing baggage: build from the scope DSC (frozen from upstream when @@ -1015,9 +1021,7 @@ sentry__span_iter_headers(sentry_value_t span, sentry__stringbuilder_append(&sb, "sentry-trace_id="); sentry__stringbuilder_append(&sb, sentry_value_as_string(trace_id)); - SENTRY_WITH_SCOPE (scope) { - sentry__scope_foreach_dsc(scope, append_baggage_member, &sb); - } + sentry__scope_foreach_dsc(scope, append_baggage_member, &sb); char *baggage = sentry__stringbuilder_into_string(&sb); if (baggage) { @@ -1046,7 +1050,9 @@ sentry_span_iter_headers(sentry_span_t *span, sentry_iter_headers_function_t callback, void *userdata) { if (span) { - sentry__span_iter_headers(span->inner, callback, userdata); + SENTRY_WITH_SCOPE (scope) { + sentry__span_iter_headers(scope, span->inner, callback, userdata); + } } } @@ -1055,7 +1061,9 @@ sentry_transaction_iter_headers(sentry_transaction_t *tx, sentry_iter_headers_function_t callback, void *userdata) { if (tx) { - sentry__span_iter_headers(tx->inner, callback, userdata); + SENTRY_WITH_SCOPE (scope) { + sentry__span_iter_headers(scope, tx->inner, callback, userdata); + } } } diff --git a/src/sentry_tracing.h b/src/sentry_tracing.h index 5c259eeb92..c3fd4d051e 100644 --- a/src/sentry_tracing.h +++ b/src/sentry_tracing.h @@ -102,4 +102,12 @@ bool sentry__trace_can_continue( sentry_value_t sentry__parse_trace_headers(const char *sentry_trace, size_t sentry_trace_len, const char *baggage, size_t baggage_len); +/** + * Iterates the distributed tracing headers of the given span, or of a + * propagation trace context, with the baggage built from the dynamic sampling + * context of `scope`. The sampled flag is omitted if `span` has none. + */ +void sentry__span_iter_headers(const sentry_scope_t *scope, sentry_value_t span, + sentry_iter_headers_function_t callback, void *userdata); + #endif diff --git a/tests/fixtures/integration.c b/tests/fixtures/integration.c index 857c20db7b..34949b1c8a 100644 --- a/tests/fixtures/integration.c +++ b/tests/fixtures/integration.c @@ -441,6 +441,13 @@ test_logger_callback( fflush(stdout); } +static void +set_header_extra(const char *key, const char *value, void *userdata) +{ + (void)userdata; + sentry_set_extra(key, sentry_value_new_string(value)); +} + static void print_envelope(sentry_envelope_t *envelope, void *unused_state) { @@ -1240,6 +1247,10 @@ main(int argc, char **argv) "sentry-environment=upstream,sentry-sample_rand=0.25"); } + if (has_arg(argc, argv, "iter-trace-headers")) { + sentry_iter_trace_propagation_headers(set_header_extra, NULL); + } + if (has_arg(argc, argv, "replay-context")) { // mimics an embedder (e.g. sentry-unreal) that stages replay clips in // `/replays/` and announces the active replay on the scope diff --git a/tests/test_integration_transactions.py b/tests/test_integration_transactions.py index 3c85ad6af9..42183f451d 100644 --- a/tests/test_integration_transactions.py +++ b/tests/test_integration_transactions.py @@ -425,6 +425,46 @@ def test_continue_trace_event(cmake, httpserver): } +@pytest.mark.parametrize("continue_trace", [False, True]) +def test_iter_trace_propagation_headers(cmake, httpserver, continue_trace): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver), SENTRY_RELEASE="🤮🚀") + + args = ["log", "iter-trace-headers", "capture-event"] + if continue_trace: + args.insert(1, "continue-trace") + run(tmp_path, "sentry_test_integration", args, env=env) + + assert len(httpserver.log) == 1 + event_req = httpserver.log[0][0] + event_body = event_req.get_data() + + event_envelope = Envelope.deserialize(event_body) + + # Extract the one-and-only-item + (event,) = event_envelope.items + + payload = event.payload.json + + # the propagated headers continue the event's trace + trace_context = payload["contexts"]["trace"] + sentry_trace = f"{trace_context['trace_id']}-{trace_context['span_id']}" + if continue_trace: + sentry_trace += "-1" + assert payload["extra"]["sentry-trace"] == sentry_trace + + # the baggage carries the event's DSC, without a deferred sampling decision + baggage = dict(m.split("=", 1) for m in payload["extra"]["baggage"].split(",")) + assert baggage.pop("sentry-trace_id") == trace_context["trace_id"] + trace_header = event_envelope.headers["trace"] + assert {f"sentry-{key}" for key in trace_header} - { + "sentry-trace_id", + "sentry-sampled", + } == set(baggage) + + def test_set_trace_transaction_scoped_event(cmake, httpserver): tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 15948b8c7e..962fcd1974 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -619,6 +619,39 @@ SENTRY_TEST(scope_continue_trace) sentry_value_decref(trace); } +static void +collect_sentry_trace(const char *key, const char *value, void *userdata) +{ + if (strcmp(key, "sentry-trace") == 0) { + snprintf((char *)userdata, 64, "%s", value); + } +} + +SENTRY_TEST(scope_iter_trace_propagation_headers) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + + // a local scope without a trace falls back to the global one + sentry_scope_t *scope = sentry_scope_new(); + char global_trace[64] = { 0 }; + char local_trace[64] = { 0 }; + sentry_iter_trace_propagation_headers(collect_sentry_trace, global_trace); + sentry_scope_iter_trace_propagation_headers( + scope, collect_sentry_trace, local_trace); + TEST_CHECK_STRING_EQUAL(local_trace, global_trace); + + sentry_scope_set_trace( + scope, "aaaabbbbccccddddeeeeffff00001111", "9c2a92b49b7c4d1e"); + sentry_scope_iter_trace_propagation_headers( + scope, collect_sentry_trace, local_trace); + TEST_CHECK( + strncmp(local_trace, "aaaabbbbccccddddeeeeffff00001111-", 33) == 0); + + sentry_scope_free(scope); + sentry_close(); +} + SENTRY_TEST(scope_extra) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/test_tracing.c b/tests/unit/test_tracing.c index f86a0b5c01..ba623c3b10 100644 --- a/tests/unit/test_tracing.c +++ b/tests/unit/test_tracing.c @@ -2809,6 +2809,60 @@ SENTRY_TEST(continue_trace_on_clone) sentry_close(); } +SENTRY_TEST(iter_trace_propagation_headers) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://k@o123456.ingest.sentry.io/1"); + sentry_options_set_transport( + options, sentry_transport_new(discard_envelope)); + sentry_options_set_traces_sample_rate(options, 1.0); + sentry_init(options); + + // no span: the propagation context with a deferred sampling decision + char sentry_trace[64]; + char baggage_prefix[64]; + SENTRY_WITH_SCOPE (scope) { + sentry_value_t trace = sentry__scope_load_trace_context(scope); + const char *trace_id = sentry_value_as_string( + sentry_value_get_by_key(trace, "trace_id")); + snprintf(sentry_trace, sizeof(sentry_trace), "%s-%s", trace_id, + sentry_value_as_string(sentry_value_get_by_key(trace, "span_id"))); + snprintf(baggage_prefix, sizeof(baggage_prefix), "sentry-trace_id=%s,", + trace_id); + sentry_value_decref(trace); + } + continuation_collector_t c = { 0 }; + sentry_iter_trace_propagation_headers(collect_continuation_headers, &c); + TEST_CHECK_STRING_EQUAL(c.sentry_trace, sentry_trace); + TEST_CHECK(strncmp(c.baggage, baggage_prefix, strlen(baggage_prefix)) == 0); + TEST_CHECK(strstr(c.baggage, "sentry-org_id=123456") != NULL); + TEST_CHECK(strstr(c.baggage, "sentry-sampled") == NULL); + + // inherited sampling decision + sentry_continue_trace(UPSTREAM_SENTRY_TRACE, "sentry-environment=upstream"); + memset(&c, 0, sizeof(c)); + sentry_iter_trace_propagation_headers(collect_continuation_headers, &c); + TEST_CHECK(strncmp(c.sentry_trace, UPSTREAM_TRACE_ID "-", 33) == 0); + TEST_CHECK(strcmp(c.sentry_trace + strlen(c.sentry_trace) - 2, "-1") == 0); + TEST_CHECK(strstr(c.baggage, "sentry-trace_id=" UPSTREAM_TRACE_ID) != NULL); + TEST_CHECK(strstr(c.baggage, "sentry-environment=upstream") != NULL); + + // bound transaction + sentry_transaction_t *tx = sentry_transaction_start( + sentry_transaction_context_new("name", "op"), sentry_value_new_null()); + sentry_set_transaction_object(tx); + continuation_collector_t tx_headers = { 0 }; + sentry_transaction_iter_headers( + tx, collect_continuation_headers, &tx_headers); + memset(&c, 0, sizeof(c)); + sentry_iter_trace_propagation_headers(collect_continuation_headers, &c); + TEST_CHECK_STRING_EQUAL(c.sentry_trace, tx_headers.sentry_trace); + TEST_CHECK_STRING_EQUAL(c.baggage, tx_headers.baggage); + + sentry_transaction_finish(tx); + sentry_close(); +} + #undef UPSTREAM_SENTRY_TRACE #undef UPSTREAM_PARENT_SPAN_ID #undef UPSTREAM_TRACE_ID diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index d52d208720..4bf1e60810 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -263,6 +263,7 @@ XX(invalid_dsn) XX(invalid_proxy) XX(is_enabled) XX(iso_time) +XX(iter_trace_propagation_headers) XX(lazy_attachments) XX(logger_enable_disable_functionality) XX(logger_level) @@ -446,6 +447,7 @@ XX(scope_global_before_init) XX(scope_global_immutable) XX(scope_isolation_reset) XX(scope_isolation_shared) +XX(scope_iter_trace_propagation_headers) XX(scope_last_event_id) XX(scope_level) XX(scope_local_attributes) From e42bdcbf406cd7da972dfe3a5206228831f1f27b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 26 Oct 2026 13:07:18 +0100 Subject: [PATCH 32/47] test: add e2e test for trace propagation (#2243) Run the integration fixture twice against Sentry: an upstream app captures an event and prints its `sentry-trace` and `baggage` headers, and a downstream app continues the trace from them and captures its own event. Sentry must then report both events on the same trace, with the upstream span as the downstream parent. --- tests/fixtures/integration.c | 18 ++++++++++++------ tests/test_e2e_sentry.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/tests/fixtures/integration.c b/tests/fixtures/integration.c index 34949b1c8a..c41b63b196 100644 --- a/tests/fixtures/integration.c +++ b/tests/fixtures/integration.c @@ -442,9 +442,10 @@ test_logger_callback( } static void -set_header_extra(const char *key, const char *value, void *userdata) +report_trace_header(const char *key, const char *value, void *userdata) { (void)userdata; + printf("%s: %s\n", key, value); sentry_set_extra(key, sentry_value_new_string(value)); } @@ -1241,14 +1242,19 @@ main(int argc, char **argv) } if (has_arg(argc, argv, "continue-trace")) { - sentry_continue_trace( - "aaaabbbbccccddddeeeeffff00001111-f0f0f0f0f0f0f0f0-1", - "sentry-trace_id=aaaabbbbccccddddeeeeffff00001111," - "sentry-environment=upstream,sentry-sample_rand=0.25"); + // the headers of an upstream app, if given + const char *sentry_trace = getenv("SENTRY_TEST_SENTRY_TRACE"); + const char *baggage = getenv("SENTRY_TEST_BAGGAGE"); + sentry_continue_trace(sentry_trace + ? sentry_trace + : "aaaabbbbccccddddeeeeffff00001111-f0f0f0f0f0f0f0f0-1", + baggage ? baggage + : "sentry-trace_id=aaaabbbbccccddddeeeeffff00001111," + "sentry-environment=upstream,sentry-sample_rand=0.25"); } if (has_arg(argc, argv, "iter-trace-headers")) { - sentry_iter_trace_propagation_headers(set_header_extra, NULL); + sentry_iter_trace_propagation_headers(report_trace_header, NULL); } if (has_arg(argc, argv, "replay-context")) { diff --git a/tests/test_e2e_sentry.py b/tests/test_e2e_sentry.py index ea2160c545..5925a8ca86 100644 --- a/tests/test_e2e_sentry.py +++ b/tests/test_e2e_sentry.py @@ -827,3 +827,39 @@ def test_e2e_crashpad(cmake): ), f"Crashpad crash should capture threads (>= 1), got {thread_count}" verify_no_thread_duplication(threads_data, "test_e2e_crashpad") + + +def test_e2e_trace_propagation(cmake): + """Verify that Sentry links the events of an upstream and a downstream app + when the downstream app continues the trace from the upstream headers.""" + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) + env = dict(os.environ, SENTRY_DSN=os.environ["SENTRY_E2E_DSN"]) + + # upstream: capture an event and print the propagation headers of its trace + output = check_output( + tmp_path, + "sentry_test_integration", + ["log", "e2e-test", "iter-trace-headers", "capture-event"], + env=env, + ) + upstream_id = extract_test_id(output) + headers = dict( + re.findall(r"^(sentry-trace|baggage): (.+)$", output.decode(), re.MULTILINE) + ) + + # downstream: continue the trace from those headers and capture an event + env["SENTRY_TEST_SENTRY_TRACE"] = headers["sentry-trace"] + env["SENTRY_TEST_BAGGAGE"] = headers["baggage"] + output = check_output( + tmp_path, + "sentry_test_integration", + ["log", "e2e-test", "continue-trace", "capture-event"], + env=env, + ) + downstream_id = extract_test_id(output) + + upstream_trace = poll_sentry_for_event(upstream_id)["contexts"]["trace"] + downstream_trace = poll_sentry_for_event(downstream_id)["contexts"]["trace"] + + assert downstream_trace["trace_id"] == upstream_trace["trace_id"] + assert downstream_trace["parent_span_id"] == upstream_trace["span_id"] From 5c720126e5cf235b2212074345911b132a3d317c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 26 Oct 2026 14:53:36 +0100 Subject: [PATCH 33/47] chore: promote `http_retry` to stable (#2246) --- include/sentry.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 258197a680..1bc7664de6 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1082,8 +1082,7 @@ SENTRY_API void sentry_transport_set_shutdown_func( * extended network outages may exhaust retry attempts that might have * succeeded with the SDK's built-in exponential backoff. */ -SENTRY_EXPERIMENTAL_API void sentry_transport_retry( - sentry_transport_t *transport); +SENTRY_API void sentry_transport_retry(sentry_transport_t *transport); /** * Generic way to free transport. @@ -3171,10 +3170,9 @@ SENTRY_API int sentry_options_get_strict_trace_continuation( * * Disabled by default. */ -SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retry( +SENTRY_API void sentry_options_set_http_retry( sentry_options_t *opts, int enabled); -SENTRY_EXPERIMENTAL_API int sentry_options_get_http_retry( - const sentry_options_t *opts); +SENTRY_API int sentry_options_get_http_retry(const sentry_options_t *opts); /** * Enables or disables attaching a stack trace to events that carry none. From 4bac98a7c1982dc80cd273dcd134e15363e35277 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 27 Oct 2026 13:34:01 +0100 Subject: [PATCH 34/47] perf(scope): avoid unnecessary empty value clones (#2251) The scope loaders for `tags`, `extra`, `contexts`, `attributes`, and `propagation_context` cloned the scope's own values before checking whether there were any. For an empty scope, the clone was freed right away, and the parent's values were returned instead. Every empty level still paid for allocating and freeing a `thing_t` handle for the empty value clone. Clone the values only when the scope has any or has no parent. An empty scope now returns the values loaded from its parent as is. The TODOs about optimizing merges into empty destinations are removed, because an empty scope no longer clones or merges anything. Measured against v1 (e42bdcbf) with a root parent and a child scope on a pinned core, with CPU frequency scaling enabled, as the median CPU time of 40 samples per case: Values Parent Child Before After Improvement strings [1] 0 0 36.7-37.2 ns 20.3-20.7 ns 44.1-45.6% objects [2] 0 0 36.3-36.5 ns 20.4-20.5 ns 43.7-44.0% strings [1] 10 0 42.3-42.7 ns 25.3-25.7 ns 39.5-40.1% objects [2] 10 0 426.7-468.6 ns 411.8-457.4 ns 2.4-3.5% strings [1] 0 10 43.4-44.1 ns 43.9-44.1 ns ~0% objects [2] 0 10 417.6-482.8 ns 421.3-474.4 ns ~0% strings [1] 10 10 1626-1653 ns 1644-1667 ns ~0% objects [2] 10 10 2468-2575 ns 2474-2562 ns ~0% [1] `tags`, `extra`, `propagation_context` [2] `contexts`, `attributes` --- src/sentry_scope.c | 46 +++++++------- tests/unit/test_scope.c | 131 ++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 5 ++ 3 files changed, 159 insertions(+), 23 deletions(-) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 15243205d7..30f3f63ca3 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -1113,16 +1113,16 @@ sentry__scope_load_propagation_context(const sentry_scope_t *scope) { sentry_value_t propagation_context = sentry_value_new_null(); SENTRY_SCOPE_READ_LOCK (scope->data) { - propagation_context - = sentry__value_clone(scope->data->propagation_context); + if (!scope->parent + || sentry_value_get_length(scope->data->propagation_context)) { + 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); + if (sentry_value_is_null(propagation_context)) { return parent; } sentry__value_merge_objects_shallow(propagation_context, parent); @@ -1752,13 +1752,13 @@ sentry__scope_load_tags(const sentry_scope_t *scope) { sentry_value_t tags = sentry_value_new_null(); SENTRY_SCOPE_READ_LOCK (scope->data) { - tags = sentry__value_clone(scope->data->tags); + if (!scope->parent || sentry_value_get_length(scope->data->tags)) { + 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); + if (sentry_value_is_null(tags)) { return parent; } sentry__value_merge_objects(tags, parent); @@ -1883,13 +1883,13 @@ sentry__scope_load_extra(const sentry_scope_t *scope) { sentry_value_t extra = sentry_value_new_null(); SENTRY_SCOPE_READ_LOCK (scope->data) { - extra = sentry__value_clone(scope->data->extra); + if (!scope->parent || sentry_value_get_length(scope->data->extra)) { + 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); + if (sentry_value_is_null(extra)) { return parent; } sentry__value_merge_objects(extra, parent); @@ -2007,14 +2007,14 @@ sentry__scope_load_attributes(const sentry_scope_t *scope) { sentry_value_t attributes = sentry_value_new_null(); SENTRY_SCOPE_READ_LOCK (scope->data) { - attributes = sentry__value_clone(scope->data->attributes); + if (!scope->parent + || sentry_value_get_length(scope->data->attributes)) { + 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); + if (sentry_value_is_null(attributes)) { return parent; } sentry__value_merge_objects_shallow(attributes, parent); @@ -2047,13 +2047,13 @@ sentry__scope_load_contexts(const sentry_scope_t *scope) { sentry_value_t contexts = sentry_value_new_null(); SENTRY_SCOPE_READ_LOCK (scope->data) { - contexts = sentry__value_clone(scope->data->contexts); + if (!scope->parent || sentry_value_get_length(scope->data->contexts)) { + 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); + if (sentry_value_is_null(contexts)) { return parent; } sentry__value_merge_objects(contexts, parent); diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 962fcd1974..5b6f74ec2d 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -14,6 +14,8 @@ TEST_CHECK(memcmp(&(Actual), &(Expected), sizeof(sentry_uuid_t)) == 0) typedef sentry_value_t (*scope_value_getter_t)(const sentry_scope_t *scope); +typedef void (*scope_value_setter_t)( + sentry_scope_t *scope, const char *key, const char *value); static sentry_value_t scope_value_get_by_key( @@ -4720,3 +4722,132 @@ SENTRY_TEST(scope_observer_init_failure) TEST_CHECK(scope->observers[0] == existing); sentry_scope_free(scope); } + +static void +set_tag(sentry_scope_t *scope, const char *key, const char *value) +{ + sentry_scope_set_tag(scope, key, value); +} + +static void +set_extra(sentry_scope_t *scope, const char *key, const char *value) +{ + sentry_scope_set_extra(scope, key, sentry_value_new_string(value)); +} + +static void +set_context(sentry_scope_t *scope, const char *key, const char *value) +{ + sentry_value_t context = sentry_value_new_object(); + sentry_value_set_by_key(context, "value", sentry_value_new_string(value)); + sentry_scope_set_context(scope, key, context); +} + +static void +set_attribute(sentry_scope_t *scope, const char *key, const char *value) +{ + sentry_scope_set_attribute(scope, key, + sentry_value_new_attribute(sentry_value_new_string(value), NULL)); +} + +static void +set_propagation_context( + sentry_scope_t *scope, const char *key, const char *value) +{ + sentry__scope_set_propagation_context( + scope, key, sentry_value_new_string(value)); +} + +static sentry_scope_t * +scope_new_child(sentry_scope_t *parent) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + sentry__scope_decref((sentry_scope_t *)scope->parent); + scope->parent = parent; + return scope; +} + +static const char * +get_string(sentry_value_t values, const char *key) +{ + sentry_value_t value = sentry_value_get_by_key(values, key); + if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_OBJECT) { + value = sentry_value_get_by_key(value, "value"); + } + return sentry_value_as_string(value); +} + +static void +check_scope_inheritance(scope_value_getter_t load, scope_value_setter_t set) +{ + sentry_scope_t *root = scope_new_child(NULL); + sentry_scope_t *middle = scope_new_child(root); + sentry_scope_t *leaf = scope_new_child(middle); + + sentry_value_t values = load(leaf); + TEST_CHECK(sentry_value_get_type(values) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(values), 0); + sentry_value_decref(values); + + // the empty middle and leaf inherit the root values + set(root, "root", "root"); + set(root, "shared", "root"); + values = load(leaf); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(values), 2); + TEST_CHECK_STRING_EQUAL(get_string(values, "root"), "root"); + TEST_CHECK_STRING_EQUAL(get_string(values, "shared"), "root"); + + // the loaded values are detached from the root + sentry_value_set_by_key(values, "root", sentry_value_new_string("changed")); + sentry_value_set_by_key(sentry_value_get_by_key(values, "shared"), "value", + sentry_value_new_string("changed")); + sentry_value_decref(values); + values = load(root); + TEST_CHECK_STRING_EQUAL(get_string(values, "root"), "root"); + TEST_CHECK_STRING_EQUAL(get_string(values, "shared"), "root"); + sentry_value_decref(values); + + // the leaf overrides the root across the empty middle + set(leaf, "leaf", "leaf"); + set(leaf, "shared", "leaf"); + values = load(leaf); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(values), 3); + TEST_CHECK_STRING_EQUAL(get_string(values, "root"), "root"); + TEST_CHECK_STRING_EQUAL(get_string(values, "leaf"), "leaf"); + TEST_CHECK_STRING_EQUAL(get_string(values, "shared"), "leaf"); + sentry_value_decref(values); + + values = load(middle); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(values), 2); + TEST_CHECK_STRING_EQUAL(get_string(values, "shared"), "root"); + sentry_value_decref(values); + + sentry_scope_free(leaf); +} + +SENTRY_TEST(scope_inheritance_tags) +{ + check_scope_inheritance(sentry__scope_load_tags, set_tag); +} + +SENTRY_TEST(scope_inheritance_extra) +{ + check_scope_inheritance(sentry__scope_load_extra, set_extra); +} + +SENTRY_TEST(scope_inheritance_contexts) +{ + check_scope_inheritance(sentry__scope_load_contexts, set_context); +} + +SENTRY_TEST(scope_inheritance_attributes) +{ + check_scope_inheritance(sentry__scope_load_attributes, set_attribute); +} + +SENTRY_TEST(scope_inheritance_propagation_context) +{ + check_scope_inheritance( + sentry__scope_load_propagation_context, set_propagation_context); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 4bf1e60810..726a42b892 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -445,6 +445,11 @@ XX(scope_fingerprint_n) XX(scope_global_attributes) XX(scope_global_before_init) XX(scope_global_immutable) +XX(scope_inheritance_attributes) +XX(scope_inheritance_contexts) +XX(scope_inheritance_extra) +XX(scope_inheritance_propagation_context) +XX(scope_inheritance_tags) XX(scope_isolation_reset) XX(scope_isolation_shared) XX(scope_iter_trace_propagation_headers) From 34b5f37f25c28309f47a0be0b34c7cf7db5ba6db Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 27 Oct 2026 16:15:18 +0200 Subject: [PATCH 35/47] feat: report host OS context under Wine, Proton and Linux containers (#2241) * Report host OS context under Wine and Proton Under Wine, build the OS context from the host via wine_get_host_version and the host os-release read through the Z: drive, in the same shape as the native Linux OS context. Prefer /run/host to report the host instead of the Steam Runtime or Flatpak container. * Update changelog * Expect host OS context in Wine integration tests * Clean up * Update CHANGELOG to remove 'Features' section Removed the 'Features' section from the CHANGELOG. * Prefer host os-release on native Linux * Report SteamOS and Bazzite as the OS name * Expect named OS context in Linux test assertions --- CHANGELOG.md | 1 + src/sentry_os.c | 358 ++++++++++++++++++++++++++++++------------- tests/assertions.py | 64 ++++++-- tests/unit/test_os.c | 75 +++++++++ tests/unit/tests.inc | 1 + 5 files changed, 383 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8620e2d552..0dc723ebc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - 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)) +- Report the host OS in the OS context under Wine and Proton, instead of the Windows version that Wine emulates. On Linux, prefer the host `os-release` inside Steam Runtime and Flatpak containers, and report SteamOS and Bazzite as the OS name. ([#2241](https://github.com/getsentry/sentry-native/pull/2241)) - Add `sentry_scope_set_trace` for per-scope traces. ([#2236](https://github.com/getsentry/sentry-native/pull/2236)) - Add `sentry_continue_trace` to continue incoming traces without a transaction. ([#2237](https://github.com/getsentry/sentry-native/pull/2237)) - Add `sentry_iter_trace_propagation_headers` to propagate traces without a span. ([#2240](https://github.com/getsentry/sentry-native/pull/2240)) diff --git a/src/sentry_os.c b/src/sentry_os.c index b6296dad73..5248d42ec6 100644 --- a/src/sentry_os.c +++ b/src/sentry_os.c @@ -6,6 +6,7 @@ #include "sentry_value.h" #include +#include #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_WINDOWS) # include "sentry_core.h" # include "sentry_utils.h" @@ -37,6 +38,166 @@ sentry__process_vm_readv(pid_t pid, const struct iovec *local_iov, } #endif +#if (defined(SENTRY_PLATFORM_WINDOWS) && !defined(SENTRY_PLATFORM_XBOX)) \ + || (defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_MACOS) \ + && !defined(SENTRY_PLATFORM_PS)) +static bool +set_os_uname(sentry_value_t os, const char *sysname, const char *release) +{ + char *version = sentry__string_clone(release); + if (!version) { + return false; + } + + char *build = version; + size_t num_dots = 0; + for (; build[0] != '\0'; build++) { + char c = build[0]; + if (c == '.') { + num_dots += 1; + } + if (!(c >= '0' && c <= '9') && (c != '.' || num_dots > 2)) { + break; + } + } + char *build_start = build; + if (build[0] == '-' || build[0] == '.') { + build_start++; + } + + if (build_start[0] != '\0') { + sentry_value_set_by_key( + os, "build", sentry_value_new_string(build_start)); + } + + build[0] = '\0'; + + sentry_value_set_by_key(os, "name", sentry_value_new_string(sysname)); + sentry_value_set_by_key(os, "version", sentry_value_new_string(version)); + sentry_free(version); + return true; +} +#endif + +#if defined(SENTRY_PLATFORM_LINUX) \ + || (defined(SENTRY_PLATFORM_WINDOWS) && !defined(SENTRY_PLATFORM_XBOX)) +# define OS_RELEASE_MAX_LINE_SIZE 256 +# define OS_RELEASE_MAX_KEY_SIZE 64 +# define OS_RELEASE_MAX_VALUE_SIZE 128 + +// Prefer the host over the Steam Runtime or Flatpak container. +static const char *const os_release_paths[] = { + "/run/host/etc/os-release", + "/run/host/usr/lib/os-release", + "/run/host/os-release", + "/etc/os-release", + "/usr/lib/os-release", +}; + +static int +parse_os_release_line(const char *line, char *key, char *value) +{ + const char *equals = strchr(line, '='); + if (equals == NULL) + return 1; + + size_t key_length + = MIN((size_t)(equals - line), OS_RELEASE_MAX_KEY_SIZE - 1); + strncpy(key, line, key_length); + key[key_length] = '\0'; + + sentry_slice_t value_slice + = { .ptr = equals + 1, .len = strlen(equals + 1) }; + + // some values are wrapped in double quotes + if (value_slice.len >= 2 && value_slice.ptr[0] == '\"' + && value_slice.ptr[value_slice.len - 1] == '\"') { + value_slice.ptr++; + value_slice.len -= 2; + } + + sentry__slice_to_buffer(value_slice, value, OS_RELEASE_MAX_VALUE_SIZE); + + return 0; +} + +static void +parse_line_into_object(const char *line, sentry_value_t os_dist) +{ + char value[OS_RELEASE_MAX_VALUE_SIZE]; + char key[OS_RELEASE_MAX_KEY_SIZE]; + + if (parse_os_release_line(line, key, value) == 0) { + if (strcmp(key, "ID") == 0) { + sentry_value_set_by_key( + os_dist, "name", sentry_value_new_string(value)); + } + + if (strcmp(key, "VERSION_ID") == 0) { + sentry_value_set_by_key( + os_dist, "version", sentry_value_new_string(value)); + } + + if (strcmp(key, "PRETTY_NAME") == 0) { + sentry_value_set_by_key( + os_dist, "pretty_name", sentry_value_new_string(value)); + } + + if (strcmp(key, "NAME") == 0) { + sentry_value_set_by_key( + os_dist, "display_name", sentry_value_new_string(value)); + } + + if (strcmp(key, "BUILD_ID") == 0) { + sentry_value_set_by_key( + os_dist, "build", sentry_value_new_string(value)); + } + } +} + +# ifndef SENTRY_UNITTEST +static +# endif + void + set_os_distribution( + sentry_value_t os, sentry_value_t os_dist, const char *release) +{ + sentry_value_t id = sentry_value_get_by_key(os_dist, "name"); + const char *id_str = sentry_value_as_string(id); + + // SteamOS and Bazzite are reported as the OS itself, not as a distribution. + if (strcmp(id_str, "steamos") == 0 || strcmp(id_str, "bazzite") == 0) { + sentry_value_t name = sentry_value_get_by_key(os_dist, "display_name"); + if (sentry_value_is_null(name)) { + name = id; + } + sentry_value_t version = sentry_value_get_by_key(os_dist, "version"); + if (!sentry_value_is_null(version)) { + sentry_value_incref(version); + sentry_value_set_by_key(os, "version", version); + } + sentry_value_set_by_key(os, "name", name); + sentry_value_set_by_key( + os, "build", sentry_value_get_by_key(os_dist, "build")); + sentry_value_set_by_key( + os, "kernel_version", sentry_value_new_string(release)); + sentry_value_incref(name); + sentry_value_incref(sentry_value_get_by_key(os_dist, "build")); + return; + } + + sentry_value_set_by_key( + os, "distribution_name", sentry_value_get_by_key(os_dist, "name")); + sentry_value_set_by_key(os, "distribution_version", + sentry_value_get_by_key(os_dist, "version")); + sentry_value_set_by_key(os, "distribution_pretty_name", + sentry_value_get_by_key(os_dist, "pretty_name")); + sentry_value_incref(sentry_value_get_by_key(os_dist, "name")); + sentry_value_incref(sentry_value_get_by_key(os_dist, "version")); + sentry_value_incref(sentry_value_get_by_key(os_dist, "pretty_name")); +} +#endif + #ifdef SENTRY_PLATFORM_WINDOWS # include @@ -402,11 +563,96 @@ sentry__get_wine_context(void) return context; } +static sentry_value_t +get_wine_os_release(const char *path) +{ + sentry_path_t *wine_path = make_wine_path(path, ""); + char *buf = read_wine_file(wine_path); + sentry__path_free(wine_path); + if (!buf) { + return sentry_value_new_null(); + } + + sentry_value_t os_dist = sentry_value_new_object(); + for (char *line = buf; line && *line;) { + char *next = strchr(line, '\n'); + if (next) { + *next++ = '\0'; + } + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\r') { + line[len - 1] = '\0'; + } + parse_line_into_object(line, os_dist); + line = next; + } + sentry_free(buf); + return os_dist; +} + +typedef void(CDECL *sentry__wine_get_host_version_t)( + const char **sysname, const char **release); + +/** + * Under Wine, reports the host OS instead of the Windows version that Wine + * emulates, in the same shape as the native Linux and macOS OS contexts. + */ +static sentry_value_t +get_wine_host_os_context(void) +{ + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + if (!ntdll) { + return sentry_value_new_null(); + } + + const sentry__wine_get_host_version_t wine_get_host_version + = (sentry__wine_get_host_version_t)GetProcAddress( + ntdll, "wine_get_host_version"); + if (!wine_get_host_version) { + return sentry_value_new_null(); + } + + const char *sysname = NULL; + const char *release = NULL; + wine_get_host_version(&sysname, &release); + if (sentry__string_empty(sysname) || sentry__string_empty(release)) { + return sentry_value_new_null(); + } + + sentry_value_t os = sentry_value_new_object(); + if (sentry_value_is_null(os) || !set_os_uname(os, sysname, release)) { + sentry_value_decref(os); + return sentry_value_new_null(); + } + + if (strcmp(sysname, "Linux") == 0) { + for (size_t i = 0; + i < sizeof(os_release_paths) / sizeof(os_release_paths[0]); i++) { + sentry_value_t os_dist = get_wine_os_release(os_release_paths[i]); + if (!sentry_value_is_null(os_dist)) { + set_os_distribution(os, os_dist, release); + sentry_value_decref(os_dist); + break; + } + } + } + + sentry_value_freeze(os); + return os; +} + # endif // !defined(SENTRY_PLATFORM_XBOX) sentry_value_t sentry__get_os_context(void) { +# if !defined(SENTRY_PLATFORM_XBOX) + sentry_value_t wine_os = get_wine_host_os_context(); + if (!sentry_value_is_null(wine_os)) { + return wine_os; + } +# endif + const sentry_value_t os = sentry_value_new_object(); if (sentry_value_is_null(os)) { return os; @@ -722,60 +968,6 @@ sentry__get_os_context(void) # include # if defined(SENTRY_PLATFORM_LINUX) -# define OS_RELEASE_MAX_LINE_SIZE 256 -# define OS_RELEASE_MAX_KEY_SIZE 64 -# define OS_RELEASE_MAX_VALUE_SIZE 128 - -static int -parse_os_release_line(const char *line, char *key, char *value) -{ - const char *equals = strchr(line, '='); - if (equals == NULL) - return 1; - - unsigned long key_length = MIN(equals - line, OS_RELEASE_MAX_KEY_SIZE - 1); - strncpy(key, line, key_length); - key[key_length] = '\0'; - - sentry_slice_t value_slice - = { .ptr = equals + 1, .len = strlen(equals + 1) }; - - // some values are wrapped in double quotes - if (value_slice.len >= 2 && value_slice.ptr[0] == '\"' - && value_slice.ptr[value_slice.len - 1] == '\"') { - value_slice.ptr++; - value_slice.len -= 2; - } - - sentry__slice_to_buffer(value_slice, value, OS_RELEASE_MAX_VALUE_SIZE); - - return 0; -} - -static void -parse_line_into_object(const char *line, sentry_value_t os_dist) -{ - char value[OS_RELEASE_MAX_VALUE_SIZE]; - char key[OS_RELEASE_MAX_KEY_SIZE]; - - if (parse_os_release_line(line, key, value) == 0) { - if (strcmp(key, "ID") == 0) { - sentry_value_set_by_key( - os_dist, "name", sentry_value_new_string(value)); - } - - if (strcmp(key, "VERSION_ID") == 0) { - sentry_value_set_by_key( - os_dist, "version", sentry_value_new_string(value)); - } - - if (strcmp(key, "PRETTY_NAME") == 0) { - sentry_value_set_by_key( - os_dist, "pretty_name", sentry_value_new_string(value)); - } - } -} - # ifndef SENTRY_UNITTEST static # endif @@ -849,60 +1041,20 @@ sentry__get_os_context(void) goto fail; } - char *build = uts.release; - size_t num_dots = 0; - for (; build[0] != '\0'; build++) { - char c = build[0]; - if (c == '.') { - num_dots += 1; - } - if (!(c >= '0' && c <= '9') && (c != '.' || num_dots > 2)) { - break; - } - } - char *build_start = build; - if (build[0] == '-' || build[0] == '.') { - build_start++; - } - - if (build_start[0] != '\0') { - sentry_value_set_by_key( - os, "build", sentry_value_new_string(build_start)); + if (!set_os_uname(os, uts.sysname, uts.release)) { + goto fail; } - build[0] = '\0'; - - sentry_value_set_by_key(os, "name", sentry_value_new_string(uts.sysname)); - sentry_value_set_by_key( - os, "version", sentry_value_new_string(uts.release)); - # if defined(SENTRY_PLATFORM_LINUX) - /** - * The file /etc/os-release takes precedence over /usr/lib/os-release. - * Applications should check for the former, and exclusively use its data if - * it exists, and only fall back to /usr/lib/os-release if it is missing. - * Applications should not read data from both files at the same time. - * - * From: - * https://www.freedesktop.org/software/systemd/man/latest/os-release.html#Description - */ - sentry_value_t os_dist = get_linux_os_release("/etc/os-release"); - if (sentry_value_is_null(os_dist)) { - os_dist = get_linux_os_release("/usr/lib/os-release"); - if (sentry_value_is_null(os_dist)) { - return os; + for (size_t i = 0; + i < sizeof(os_release_paths) / sizeof(os_release_paths[0]); i++) { + sentry_value_t os_dist = get_linux_os_release(os_release_paths[i]); + if (!sentry_value_is_null(os_dist)) { + set_os_distribution(os, os_dist, uts.release); + sentry_value_decref(os_dist); + break; } } - sentry_value_set_by_key( - os, "distribution_name", sentry_value_get_by_key(os_dist, "name")); - sentry_value_set_by_key(os, "distribution_version", - sentry_value_get_by_key(os_dist, "version")); - sentry_value_set_by_key(os, "distribution_pretty_name", - sentry_value_get_by_key(os_dist, "pretty_name")); - sentry_value_incref(sentry_value_get_by_key(os_dist, "name")); - sentry_value_incref(sentry_value_get_by_key(os_dist, "version")); - sentry_value_incref(sentry_value_get_by_key(os_dist, "pretty_name")); - sentry_value_decref(os_dist); # endif // defined(SENTRY_PLATFORM_LINUX) return os; diff --git a/tests/assertions.py b/tests/assertions.py index 864506094e..1440efebf5 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -13,7 +13,7 @@ import msgpack from . import REPLAY_ID, SENTRY_VERSION -from .conditions import is_android, is_asan, is_tsan, is_wine +from .conditions import is_android, is_asan, is_tsan VERSION_RE = re.compile(r"(\d+\.\d+\.\d+)[-.]?(.*)") @@ -22,6 +22,31 @@ ) +OS_RELEASE_PATHS = [ + "/run/host/etc/os-release", + "/run/host/usr/lib/os-release", + "/run/host/os-release", + "/etc/os-release", + "/usr/lib/os-release", +] + + +def read_os_release(): + for path in OS_RELEASE_PATHS: + try: + with open(path) as f: + lines = f.read().splitlines() + except OSError: + continue + fields = {} + for line in lines: + key, sep, value = line.partition("=") + if sep: + fields[key] = value.strip('"') + return fields + return {} + + def matches(actual, expected): return {k: v for (k, v) in actual.items() if k in expected.keys()} == expected @@ -119,11 +144,11 @@ def assert_event_meta( if is_android: expected_sdk["name"] = "sentry.native.android" else: - if sys.platform == "win32" or is_wine: - expected_os_context = {"name": "Windows"} - if not is_wine: - expected_os_context["version"] = platform.version() - assert_matches(event["contexts"]["os"], expected_os_context) + if sys.platform == "win32": + assert_matches( + event["contexts"]["os"], + {"name": "Windows", "version": platform.version()}, + ) assert event["contexts"]["os"]["version"] is not None assert event["contexts"]["os"]["build"] is not None elif sys.platform == "linux": @@ -131,13 +156,26 @@ def assert_event_meta( match = VERSION_RE.match(version) version = match.group(1) build = match.group(2) - expected_os_context = {"name": "Linux", "version": version} - if build: - expected_os_context["build"] = build - - assert_matches(event["contexts"]["os"], expected_os_context) - assert "distribution_name" in event["contexts"]["os"] - assert "distribution_version" in event["contexts"]["os"] + os_release = read_os_release() + if os_release.get("ID") in ("steamos", "bazzite"): + expected_os_context = { + "name": os_release.get("NAME", os_release["ID"]), + "version": os_release.get("VERSION_ID", version), + "kernel_version": platform.release(), + } + if "BUILD_ID" in os_release: + expected_os_context["build"] = os_release["BUILD_ID"] + + assert_matches(event["contexts"]["os"], expected_os_context) + assert "distribution_name" not in event["contexts"]["os"] + else: + expected_os_context = {"name": "Linux", "version": version} + if build: + expected_os_context["build"] = build + + assert_matches(event["contexts"]["os"], expected_os_context) + assert "distribution_name" in event["contexts"]["os"] + assert "distribution_version" in event["contexts"]["os"] elif sys.platform == "darwin": version = platform.mac_ver()[0].split(".") if len(version) < 3: diff --git a/tests/unit/test_os.c b/tests/unit/test_os.c index 3c9e2a3437..be8e64cc4c 100644 --- a/tests/unit/test_os.c +++ b/tests/unit/test_os.c @@ -129,6 +129,24 @@ assert_equals_snap(sentry_value_t os_dist) } extern sentry_value_t get_linux_os_release(const char *os_rel_path); +extern void set_os_distribution( + sentry_value_t os, sentry_value_t os_dist, const char *release); + +static sentry_value_t +make_linux_os(void) +{ + sentry_value_t os = sentry_value_new_object(); + sentry_value_set_by_key(os, "name", sentry_value_new_string("Linux")); + sentry_value_set_by_key(os, "version", sentry_value_new_string("6.1.0")); + sentry_value_set_by_key(os, "build", sentry_value_new_string("test")); + return os; +} + +static const char * +os_string(sentry_value_t os, const char *key) +{ + return sentry_value_as_string(sentry_value_get_by_key(os, key)); +} #endif // defined(SENTRY_PLATFORM_LINUX) && !defined(SENTRY_PLATFORM_ANDROID) SENTRY_TEST(os_releases_snapshot) @@ -195,6 +213,63 @@ SENTRY_TEST(os_release_non_existent_files) #endif // !defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) } +SENTRY_TEST(os_release_named_distributions) +{ +#if !defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + SKIP_TEST(); +#else + sentry_value_t os = make_linux_os(); + sentry_value_t os_dist = sentry_value_new_object(); + sentry_value_set_by_key( + os_dist, "name", sentry_value_new_string("steamos")); + sentry_value_set_by_key( + os_dist, "display_name", sentry_value_new_string("SteamOS")); + sentry_value_set_by_key(os_dist, "version", sentry_value_new_string("3.7")); + sentry_value_set_by_key( + os_dist, "build", sentry_value_new_string("20250101.1")); + set_os_distribution(os, os_dist, "6.1.0-test"); + TEST_CHECK_STRING_EQUAL(os_string(os, "name"), "SteamOS"); + TEST_CHECK_STRING_EQUAL(os_string(os, "version"), "3.7"); + TEST_CHECK_STRING_EQUAL(os_string(os, "build"), "20250101.1"); + TEST_CHECK_STRING_EQUAL(os_string(os, "kernel_version"), "6.1.0-test"); + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(os, "distribution_name"))); + sentry_value_decref(os_dist); + sentry_value_decref(os); + + // falls back to ID and the kernel version, and drops the kernel build + os = make_linux_os(); + os_dist = sentry_value_new_object(); + sentry_value_set_by_key( + os_dist, "name", sentry_value_new_string("bazzite")); + set_os_distribution(os, os_dist, "6.1.0-test"); + TEST_CHECK_STRING_EQUAL(os_string(os, "name"), "bazzite"); + TEST_CHECK_STRING_EQUAL(os_string(os, "version"), "6.1.0"); + TEST_CHECK(sentry_value_is_null(sentry_value_get_by_key(os, "build"))); + TEST_CHECK_STRING_EQUAL(os_string(os, "kernel_version"), "6.1.0-test"); + sentry_value_decref(os_dist); + sentry_value_decref(os); + + os = make_linux_os(); + os_dist = sentry_value_new_object(); + sentry_value_set_by_key(os_dist, "name", sentry_value_new_string("ubuntu")); + sentry_value_set_by_key( + os_dist, "display_name", sentry_value_new_string("Ubuntu")); + sentry_value_set_by_key( + os_dist, "version", sentry_value_new_string("24.04")); + set_os_distribution(os, os_dist, "6.1.0-test"); + TEST_CHECK_STRING_EQUAL(os_string(os, "name"), "Linux"); + TEST_CHECK_STRING_EQUAL(os_string(os, "version"), "6.1.0"); + TEST_CHECK_STRING_EQUAL(os_string(os, "build"), "test"); + TEST_CHECK_STRING_EQUAL(os_string(os, "distribution_name"), "ubuntu"); + TEST_CHECK_STRING_EQUAL(os_string(os, "distribution_version"), "24.04"); + TEST_CHECK( + sentry_value_is_null(sentry_value_get_by_key(os, "kernel_version"))); + sentry_value_decref(os_dist); + sentry_value_decref(os); +#endif // !defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +} + #ifdef SENTRY_PLATFORM_WINDOWS extern BOOL(WINAPI *g_kernel32_SetThreadStackGuarantee)(PULONG); extern void(WINAPI *g_kernel32_GetCurrentThreadStackLimits)( diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 726a42b892..33b2c78ce2 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -343,6 +343,7 @@ XX(options_thread_stackwalk_mode_clamp) XX(options_thread_stackwalk_mode_default) XX(options_thread_stackwalk_mode_set_get) XX(os) +XX(os_release_named_distributions) XX(os_release_non_existent_files) XX(os_releases_snapshot) XX(overflow_spans) From 5d930d5c9c023c80d9dd1a5e6b232be3ddcc6f51 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Oct 2026 12:22:22 +0100 Subject: [PATCH 36/47] feat(native): support ELF modules at non-zero file offsets (#2265) * feat(native): Support ELF modules at non-zero file offsets The minidump writer and `capture_modules_from_proc_maps()` grouped `/proc//maps` entries by file name and expected an ELF header at offset 0. An ELF image can also start inside a larger file, such as an uncompressed library that Android maps straight from an APK, and such images were dropped from the module list. Start a module at each mapping whose file offset holds an ELF header, group the image's other segments by its load range, and read the build ID, size, and SONAME at that offset. Move `compute_elf_size_from_phdrs()` to `sentry_elf.h` to share it. This prepares the native backend for Android, where AGP's default packaging maps every app library from the APK. Ref: #1725 * fix(native): Apply the ELF offset to the build ID section headers The previous commit adjusted the section header offset in `elf_locate_symtab()` instead of `extract_elf_build_id_for_module()`, where `elf_offset` is not declared, so the crash daemon failed to compile. Move the adjustment to where the build ID is read. * fix(native): Keep embedded ELFs mapped below their file offset The `offset > start` guard in `capture_modules_from_proc_maps()` dropped mappings before an ELF image inside a larger file could be detected. The guard only protects the `start - offset` load base of a regular ELF file, so apply it there instead. --- .../native/minidump/sentry_minidump_linux.c | 148 ++++++------------ src/backends/native/sentry_crash_context.h | 2 + src/backends/native/sentry_crash_daemon.c | 54 ++++--- src/sentry_elf.h | 94 ++++++++++- tests/unit/test_native_backend.c | 21 +++ tests/unit/tests.inc | 1 + 6 files changed, 199 insertions(+), 121 deletions(-) diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index 39a2d5be97..25c314ea16 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -862,11 +862,12 @@ write_thread_context( } /** - * Extract Build ID from ELF file + * Extract Build ID from the ELF image at `elf_offset` in a file * Returns the Build ID length, or 0 if not found */ static size_t -extract_elf_build_id(const char *elf_path, uint8_t *build_id, size_t max_len) +extract_elf_build_id(const char *elf_path, uint64_t elf_offset, + uint8_t *build_id, size_t max_len) { int fd = sentry__elf_open(elf_path); if (fd < 0) { @@ -880,7 +881,7 @@ extract_elf_build_id(const char *elf_path, uint8_t *build_id, size_t max_len) Elf32_Ehdr ehdr; # endif - if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) { + if (pread(fd, &ehdr, sizeof(ehdr), (off_t)elf_offset) != sizeof(ehdr)) { close(fd); return 0; } @@ -906,7 +907,8 @@ extract_elf_build_id(const char *elf_path, uint8_t *build_id, size_t max_len) return 0; } - if (lseek(fd, ehdr.e_shoff, SEEK_SET) != (off_t)ehdr.e_shoff + uint64_t shoff = elf_offset + ehdr.e_shoff; + if (lseek(fd, (off_t)shoff, SEEK_SET) != (off_t)shoff || read(fd, shdr_buf, shdr_size) != (ssize_t)shdr_size) { sentry_free(shdr_buf); close(fd); @@ -932,8 +934,8 @@ extract_elf_build_id(const char *elf_path, uint8_t *build_id, size_t max_len) if (!note_buf) continue; - if (lseek(fd, sections[i].sh_offset, SEEK_SET) - == (off_t)sections[i].sh_offset + uint64_t note_offset = elf_offset + sections[i].sh_offset; + if (lseek(fd, (off_t)note_offset, SEEK_SET) == (off_t)note_offset && read(fd, note_buf, note_size) == (ssize_t)note_size) { // Find the GNU build-id note. SHT_NOTE uses 4-byte alignment. size_t desc_size = 0; @@ -958,87 +960,6 @@ extract_elf_build_id(const char *elf_path, uint8_t *build_id, size_t max_len) return build_id_len; } -/** - * Compute the virtual memory size of an ELF file from its PT_LOAD segments. - * This matches Crashpad's behavior: size = max(p_vaddr + p_memsz) - - * min(p_vaddr) across all PT_LOAD segments, rather than using page-aligned - * /proc/maps sizes. - * Returns the computed size, or 0 on failure (caller should fall back to - * /proc/maps size). - */ -static uint64_t -compute_elf_size_from_phdrs(const char *elf_path) -{ - int fd = sentry__elf_open(elf_path); - if (fd < 0) { - return 0; - } - -# if defined(__x86_64__) || defined(__aarch64__) - Elf64_Ehdr ehdr; -# else - Elf32_Ehdr ehdr; -# endif - - if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr) - || memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0 || ehdr.e_phnum == 0) { - close(fd); - return 0; - } - - if (!sentry__elf_has_phdr_size(ehdr.e_ident, ehdr.e_phentsize)) { - close(fd); - return 0; - } - - // Read program headers - size_t phdr_size = (size_t)ehdr.e_phentsize * ehdr.e_phnum; - void *phdr_buf = sentry_malloc(phdr_size); - if (!phdr_buf) { - close(fd); - return 0; - } - - if (lseek(fd, ehdr.e_phoff, SEEK_SET) != (off_t)ehdr.e_phoff - || read(fd, phdr_buf, phdr_size) != (ssize_t)phdr_size) { - sentry_free(phdr_buf); - close(fd); - return 0; - } - -# if defined(__x86_64__) || defined(__aarch64__) - Elf64_Phdr *phdrs = (Elf64_Phdr *)phdr_buf; -# else - Elf32_Phdr *phdrs = (Elf32_Phdr *)phdr_buf; -# endif - - uint64_t min_vaddr = UINT64_MAX; - uint64_t max_vaddr = 0; - bool found_load = false; - - for (int i = 0; i < ehdr.e_phnum; i++) { - if (phdrs[i].p_type == PT_LOAD) { - found_load = true; - if (phdrs[i].p_vaddr < min_vaddr) { - min_vaddr = phdrs[i].p_vaddr; - } - uint64_t end = phdrs[i].p_vaddr + phdrs[i].p_memsz; - if (end > max_vaddr) { - max_vaddr = end; - } - } - } - - sentry_free(phdr_buf); - close(fd); - - if (!found_load || max_vaddr <= min_vaddr) { - return 0; - } - - return max_vaddr - min_vaddr; -} - /** * Read the DT_SONAME from an ELF file's dynamic section. * Crashpad uses SONAME as the module name, falling back to the /proc/maps @@ -1047,7 +968,8 @@ compute_elf_size_from_phdrs(const char *elf_path) * Returns true if SONAME was found and written to soname_buf. */ static bool -read_elf_soname(const char *elf_path, char *soname_buf, size_t soname_buf_size) +read_elf_soname(const char *elf_path, uint64_t elf_offset, char *soname_buf, + size_t soname_buf_size) { int fd = sentry__elf_open(elf_path); if (fd < 0) { @@ -1066,7 +988,7 @@ read_elf_soname(const char *elf_path, char *soname_buf, size_t soname_buf_size) bool result = false; - if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr) + if (pread(fd, &ehdr, sizeof(ehdr), (off_t)elf_offset) != sizeof(ehdr) || memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0) { close(fd); return false; @@ -1085,7 +1007,8 @@ read_elf_soname(const char *elf_path, char *soname_buf, size_t soname_buf_size) return false; } - if (lseek(fd, ehdr.e_shoff, SEEK_SET) != (off_t)ehdr.e_shoff + uint64_t shoff = elf_offset + ehdr.e_shoff; + if (lseek(fd, (off_t)shoff, SEEK_SET) != (off_t)shoff || read(fd, shdr_buf, shdr_size) != (ssize_t)shdr_size) { sentry_free(shdr_buf); close(fd); @@ -1123,7 +1046,7 @@ read_elf_soname(const char *elf_path, char *soname_buf, size_t soname_buf_size) // Read .dynstr char *dynstr = sentry__elf_read_metadata_section( - fd, dynstr_shdr->sh_offset, dynstr_shdr->sh_size); + fd, elf_offset + dynstr_shdr->sh_offset, dynstr_shdr->sh_size); if (!dynstr) { sentry_free(shdr_buf); close(fd); @@ -1133,7 +1056,7 @@ read_elf_soname(const char *elf_path, char *soname_buf, size_t soname_buf_size) // Read .dynamic entries and find DT_SONAME void *dyn_buf = sentry__elf_read_metadata_section( - fd, dynamic_shdr->sh_offset, dynamic_shdr->sh_size); + fd, elf_offset + dynamic_shdr->sh_offset, dynamic_shdr->sh_size); if (!dyn_buf) { sentry_free(dynstr); sentry_free(shdr_buf); @@ -1540,6 +1463,7 @@ typedef struct { uint64_t end; // End of last mapping for this file char *name; // Pointer into mappings[].name (not owned) char soname[256]; // SONAME from ELF dynamic section (empty if not found) + uint64_t elf_offset; // File offset of the ELF image (non-zero in an APK) uint64_t elf_size; // Size from PT_LOAD segments (0 = use maps size) uint8_t build_id[32]; size_t build_id_len; @@ -1562,6 +1486,11 @@ typedef struct { * * base_of_image is set to the address of the offset==0 mapping — the real * ELF load address — which is what Breakpad/Crashpad/rust-minidump expect. + * + * Android maps uncompressed libraries straight from the APK, so one file can + * hold several ELF images. Such an image starts at the mapping whose file + * offset holds the ELF header, and its other segments are grouped by the + * image's load range instead of by file name. */ static size_t resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, @@ -1596,15 +1525,21 @@ resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, break; } } - if (already_skipped) { + // A non-ELF file can still hold ELF images at non-zero offsets. + if (already_skipped && mapping->offset == 0) { continue; } // Find an existing module entry by name. This merges across // non-contiguous /proc/maps lines — fixes duplicate ld-linux entries. + // An ELF image inside a larger file only owns its load range. resolved_module_t *mod = NULL; for (size_t j = 0; j < module_count; j++) { - if (strcmp(modules[j].name, mapping->name) == 0) { + if (strcmp(modules[j].name, mapping->name) == 0 + && (modules[j].elf_offset == 0 + || (mapping->start >= modules[j].base + && mapping->start + < modules[j].base + modules[j].elf_size))) { mod = &modules[j]; break; } @@ -1627,10 +1562,18 @@ resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, continue; } + // An ELF image can also start inside a larger file, such as a library + // that Android maps straight from an APK. + bool embedded = mapping->offset != 0 + && sentry__elf_is_file_at(mapping->name, mapping->offset); + if (!embedded && already_skipped) { + continue; + } + // First time we see this file — confirm it's actually an ELF before // adding it as a module. This drops sentry-native's own IPC shm, // deleted semaphores, and any other non-ELF named mapping. - if (!sentry__elf_is_file(mapping->name)) { + if (!embedded && !sentry__elf_is_file(mapping->name)) { if (skip_count < SENTRY_CRASH_MAX_MODULES) { skip_names[skip_count++] = mapping->name; } @@ -1647,15 +1590,18 @@ resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, mod->name = (char *)mapping->name; mod->base = mapping->start; mod->end = mapping->end; + mod->elf_offset = embedded ? mapping->offset : 0; + mod->elf_size + = sentry__elf_compute_size_from_phdrs(mod->name, mod->elf_offset); } - // Extract Build IDs, ELF sizes, and SONAMEs for each resolved module + // Extract Build IDs and SONAMEs for each resolved module for (size_t i = 0; i < module_count; i++) { - modules[i].build_id_len = extract_elf_build_id( - modules[i].name, modules[i].build_id, sizeof(modules[i].build_id)); - modules[i].elf_size = compute_elf_size_from_phdrs(modules[i].name); - if (!read_elf_soname(modules[i].name, modules[i].soname, - sizeof(modules[i].soname))) { + modules[i].build_id_len + = extract_elf_build_id(modules[i].name, modules[i].elf_offset, + modules[i].build_id, sizeof(modules[i].build_id)); + if (!read_elf_soname(modules[i].name, modules[i].elf_offset, + modules[i].soname, sizeof(modules[i].soname))) { modules[i].soname[0] = '\0'; } } diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index 359df783fb..8a64cf6a36 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -155,6 +155,8 @@ typedef struct { uint32_t pdb_age; // PDB age (Windows PE only, appended to debug_id) #if defined(SENTRY_PLATFORM_WINDOWS) char pdb_name[SENTRY_CRASH_MAX_PATH]; // PDB filename (Windows PE only) +#elif defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + uint64_t elf_offset; // File offset of the ELF image (non-zero in an APK) #endif } sentry_module_info_t; diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 44a296875a..2888664686 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -1272,12 +1272,13 @@ build_stacktrace_for_thread( # include /** - * Extract Build ID from ELF file (for debug_meta) + * Extract Build ID from the ELF image at `elf_offset` in a file + * (for debug_meta) * Returns the Build ID length, or 0 if not found */ static size_t -extract_elf_build_id_for_module( - const char *elf_path, uint8_t *build_id, size_t max_len) +extract_elf_build_id_for_module(const char *elf_path, uint64_t elf_offset, + uint8_t *build_id, size_t max_len) { int fd = sentry__elf_open(elf_path); if (fd < 0) { @@ -1291,7 +1292,7 @@ extract_elf_build_id_for_module( Elf32_Ehdr ehdr; # endif - if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) { + if (pread(fd, &ehdr, sizeof(ehdr), (off_t)elf_offset) != sizeof(ehdr)) { close(fd); return 0; } @@ -1315,7 +1316,8 @@ extract_elf_build_id_for_module( return 0; } - if (lseek(fd, ehdr.e_shoff, SEEK_SET) != (off_t)ehdr.e_shoff + uint64_t shoff = elf_offset + ehdr.e_shoff; + if (lseek(fd, (off_t)shoff, SEEK_SET) != (off_t)shoff || read(fd, shdr_buf, shdr_size) != (ssize_t)shdr_size) { sentry_free(shdr_buf); close(fd); @@ -1345,8 +1347,8 @@ extract_elf_build_id_for_module( continue; } - if (lseek(fd, sections[i].sh_offset, SEEK_SET) - != (off_t)sections[i].sh_offset + uint64_t note_offset = elf_offset + sections[i].sh_offset; + if (lseek(fd, (off_t)note_offset, SEEK_SET) != (off_t)note_offset || read(fd, note_buf, note_size) != (ssize_t)note_size) { sentry_free(note_buf); continue; @@ -1571,7 +1573,7 @@ get_sym_source(const sentry_module_info_t *mod, uint32_t mod_idx) if (debuglink[0]) { uint8_t mod_bid[16]; size_t mod_bid_len = extract_elf_build_id_for_module( - mod->name, mod_bid, sizeof(mod_bid)); + mod->name, mod->elf_offset, mod_bid, sizeof(mod_bid)); const char *slash = strrchr(mod->name, '/'); int dir_len = slash ? (int)(slash - mod->name) : 0; @@ -1605,7 +1607,7 @@ get_sym_source(const sentry_module_info_t *mod, uint32_t mod_idx) uint8_t dbg_bid[16]; size_t dbg_bid_len = extract_elf_build_id_for_module( - candidate, dbg_bid, sizeof(dbg_bid)); + candidate, 0, dbg_bid, sizeof(dbg_bid)); if (mod_bid_len && dbg_bid_len && (mod_bid_len != dbg_bid_len || memcmp(mod_bid, dbg_bid, mod_bid_len) != 0)) { @@ -1816,20 +1818,26 @@ 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 = ctx->module_count; j > 0; j--) { - if (strncmp(ctx->modules[j - 1].name, pathname, len) == 0 - && ctx->modules[j - 1].name[len] == '\0') { + sentry_module_info_t *other = &ctx->modules[j - 1]; + if (strncmp(other->name, pathname, len) == 0 + && other->name[len] == '\0') { + // An ELF image inside a larger file only owns its load range + if (other->elf_offset != 0) { + if (start >= other->base_address + && start < other->base_address + other->size) { + existing_mod = other; + break; + } + continue; + } if (offset != 0 || j == ctx->module_count) { - existing_mod = &ctx->modules[j - 1]; + existing_mod = other; } break; } @@ -1863,7 +1871,17 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) memcpy(mod->name, pathname, copy_len); mod->name[copy_len] = '\0'; - if (!sentry__elf_is_file(mod->name)) { + // An ELF image can also start inside a larger file, such as a library + // that Android maps straight from an APK. Its load size is known up + // front to group the image's other segments. + mod->elf_offset = 0; + if (offset != 0 && sentry__elf_is_file_at(mod->name, offset)) { + uint64_t elf_size + = sentry__elf_compute_size_from_phdrs(mod->name, offset); + mod->elf_offset = offset; + mod->base_address = start; + mod->size = MAX(end - start, elf_size); + } else if (offset > start || !sentry__elf_is_file(mod->name)) { continue; } @@ -1871,7 +1889,7 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) memset(mod->uuid, 0, sizeof(mod->uuid)); mod->pdb_age = 0; // Not used on Linux, only for Windows PE modules extract_elf_build_id_for_module( - mod->name, mod->uuid, sizeof(mod->uuid)); + mod->name, mod->elf_offset, mod->uuid, sizeof(mod->uuid)); // Convert to little-endian GUID format for Sentry debug_id sentry__uuid_swap_guid_bytes(mod->uuid); diff --git a/src/sentry_elf.h b/src/sentry_elf.h index bb80d3f019..9c161bce0d 100644 --- a/src/sentry_elf.h +++ b/src/sentry_elf.h @@ -37,9 +37,11 @@ sentry__elf_open(const char *path) * Used to filter out non-ELF mappings (e.g. files under /dev/shm, deleted * files, sentry-native's own IPC shared memory) from the module list — LLDB and * other consumers can choke on entries that look like modules but aren't. + * A non-zero offset checks for an ELF image inside a larger file, such as a + * library that Android maps straight from an APK. */ static inline bool -sentry__elf_is_file(const char *path) +sentry__elf_is_file_at(const char *path, uint64_t offset) { int fd = sentry__elf_open(path); if (fd < 0) { @@ -47,12 +49,18 @@ sentry__elf_is_file(const char *path) } unsigned char magic[SELFMAG]; - bool is_elf = read(fd, magic, SELFMAG) == (ssize_t)SELFMAG + bool is_elf = pread(fd, magic, SELFMAG, (off_t)offset) == (ssize_t)SELFMAG && memcmp(magic, ELFMAG, SELFMAG) == 0; close(fd); return is_elf; } +static inline bool +sentry__elf_is_file(const char *path) +{ + return sentry__elf_is_file_at(path, 0); +} + static inline bool sentry__elf_is_native_class(const unsigned char e_ident[EI_NIDENT]) { @@ -203,6 +211,88 @@ sentry__elf_find_note(const void *buf, size_t buf_size, size_t alignment, return NULL; } +/** + * Compute the virtual memory size of an ELF file from its PT_LOAD segments. + * This matches Crashpad's behavior: size = max(p_vaddr + p_memsz) - + * min(p_vaddr) across all PT_LOAD segments, rather than using page-aligned + * /proc/maps sizes. + * Returns the computed size, or 0 on failure (caller should fall back to + * /proc/maps size). + */ +static inline uint64_t +sentry__elf_compute_size_from_phdrs(const char *elf_path, uint64_t elf_offset) +{ + int fd = sentry__elf_open(elf_path); + if (fd < 0) { + return 0; + } + +# if defined(__x86_64__) || defined(__aarch64__) + Elf64_Ehdr ehdr; +# else + Elf32_Ehdr ehdr; +# endif + + if (pread(fd, &ehdr, sizeof(ehdr), (off_t)elf_offset) != sizeof(ehdr) + || memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0 || ehdr.e_phnum == 0) { + close(fd); + return 0; + } + + if (!sentry__elf_has_phdr_size(ehdr.e_ident, ehdr.e_phentsize)) { + close(fd); + return 0; + } + + // Read program headers + size_t phdr_size = (size_t)ehdr.e_phentsize * ehdr.e_phnum; + void *phdr_buf = sentry_malloc(phdr_size); + if (!phdr_buf) { + close(fd); + return 0; + } + + uint64_t phoff = elf_offset + ehdr.e_phoff; + if (lseek(fd, (off_t)phoff, SEEK_SET) != (off_t)phoff + || read(fd, phdr_buf, phdr_size) != (ssize_t)phdr_size) { + sentry_free(phdr_buf); + close(fd); + return 0; + } + +# if defined(__x86_64__) || defined(__aarch64__) + Elf64_Phdr *phdrs = (Elf64_Phdr *)phdr_buf; +# else + Elf32_Phdr *phdrs = (Elf32_Phdr *)phdr_buf; +# endif + + uint64_t min_vaddr = UINT64_MAX; + uint64_t max_vaddr = 0; + bool found_load = false; + + for (int i = 0; i < ehdr.e_phnum; i++) { + if (phdrs[i].p_type == PT_LOAD) { + found_load = true; + if (phdrs[i].p_vaddr < min_vaddr) { + min_vaddr = phdrs[i].p_vaddr; + } + uint64_t end = phdrs[i].p_vaddr + phdrs[i].p_memsz; + if (end > max_vaddr) { + max_vaddr = end; + } + } + } + + sentry_free(phdr_buf); + close(fd); + + if (!found_load || max_vaddr <= min_vaddr) { + return 0; + } + + return max_vaddr - min_vaddr; +} + #endif #endif diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index db7f884d86..02c2fdfab9 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -242,6 +242,27 @@ SENTRY_TEST(elf_rejects_non_regular_files) #endif } +SENTRY_TEST(elf_offset) +{ +#if !defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + SKIP_TEST(); +#else + // an APK-like zip with an uncompressed library at a page-aligned offset + char path[] = "/tmp/sentry-minidump-apk-XXXXXX"; + int fd = mkstemp(path); + TEST_ASSERT(fd >= 0); + TEST_ASSERT(write(fd, "PK\3\4", 4) == 4); + TEST_ASSERT(pwrite(fd, ELFMAG, SELFMAG, 4096) == (ssize_t)SELFMAG); + close(fd); + + TEST_CHECK(!sentry__elf_is_file(path)); + TEST_CHECK(sentry__elf_is_file_at(path, 4096)); + TEST_CHECK(!sentry__elf_is_file_at(path, 8192)); + + TEST_CHECK(unlink(path) == 0); +#endif +} + /** * Test minidump directory entry structure */ diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 096fb422e3..3dbb8d3e31 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -206,6 +206,7 @@ XX(effective_org_id_resolution) XX(elf_find_note) XX(elf_header_entry_sizes) XX(elf_metadata_section) +XX(elf_offset) XX(elf_rejects_non_regular_files) XX(embedded_info_basic) XX(embedded_info_build_id) From fe653549059c5ecf196b765f4c01439b25bd1b73 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Oct 2026 12:22:22 +0100 Subject: [PATCH 37/47] feat(unwinder): Unwind remote threads with libunwindstack (#2267) The crash daemon could only unwind the threads that did not crash with `libunwind_remote`, which Android builds lack because they use libunwindstack. Add a `libunwindstack_remote` backend, compiled in only with the native backend, that attaches to a thread with ptrace, reads its registers with `Regs::RemoteGet()`, and unwinds it. Share the ptrace attach loop with `libunwind_remote`. This prepares the native backend for Android. --- CMakeLists.txt | 5 + src/CMakeLists.txt | 6 + src/backends/native/sentry_crash_daemon.c | 14 +- src/unwinder/sentry_unwinder.c | 66 ++++++++++ src/unwinder/sentry_unwinder.h | 2 + .../sentry_unwinder_libunwind_remote.c | 39 +----- .../sentry_unwinder_libunwindstack_remote.cpp | 120 ++++++++++++++++++ 7 files changed, 211 insertions(+), 41 deletions(-) create mode 100644 src/unwinder/sentry_unwinder_libunwindstack_remote.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d967bfddd..b6d1e85715 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -978,6 +978,11 @@ elseif(SENTRY_BACKEND_NATIVE) ${PROJECT_SOURCE_DIR}/src ${PROJECT_SOURCE_DIR}/src/backends/native ) + if(SENTRY_WITH_LIBUNWINDSTACK) + target_include_directories(sentry-crash PRIVATE + ${PROJECT_SOURCE_DIR}/external/libunwindstack-ndk/include + ) + endif() # Link same libraries as sentry target_link_libraries(sentry-crash PRIVATE ${_SENTRY_PLATFORM_LIBS}) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 11ee4f4c9b..2aa4739b79 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -178,6 +178,12 @@ elseif(SENTRY_BACKEND_NATIVE) backends/native/sentry_crash_ipc.c backends/native/sentry_crash_handler.c ) + if(SENTRY_WITH_LIBUNWINDSTACK) + target_compile_definitions(sentry PRIVATE SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE) + sentry_target_sources_cwd(sentry + unwinder/sentry_unwinder_libunwindstack_remote.cpp + ) + endif() # Add include directory for native backend headers target_include_directories(sentry PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/backends/native) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 2888664686..118b8fa9a7 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -536,7 +536,8 @@ build_registers_from_ctx(const sentry_crash_context_t *ctx, size_t thread_idx) return sentry__build_registers(&uctx); } -#ifdef SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE +#if defined(SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE) \ + || defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE) static sentry_value_t build_registers_from_remote_registers( const sentry_remote_registers_t *remote_registers) @@ -1002,10 +1003,11 @@ build_stacktrace_for_thread( int frame_count = 0; #if defined(SENTRY_PLATFORM_LINUX) - // Remote DWARF unwinding via libunwind ptrace accessors. Do not attach to + // Remote DWARF unwinding via ptrace. Do not attach to // the crashed thread from the daemon; use the saved fault context below. { -# ifdef SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE +# if defined(SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE) \ + || defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE) pid_t tid = 0; if (thread_idx == SIZE_MAX) { tid = ctx->crashed_tid; @@ -1075,6 +1077,12 @@ build_stacktrace_for_thread( sentry_value_set_by_key(stacktrace, "registers", build_registers_from_remote_registers(®isters)); } +# ifdef SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE + // libunwindstack already adjusts the caller frames + sentry_value_set_by_key(stacktrace, + "instruction_addr_adjustment", + sentry_value_new_string("none")); +# endif sentry_free(remote_frames); return stacktrace; } diff --git a/src/unwinder/sentry_unwinder.c b/src/unwinder/sentry_unwinder.c index 420dd1f26e..e3aa67ce60 100644 --- a/src/unwinder/sentry_unwinder.c +++ b/src/unwinder/sentry_unwinder.c @@ -1,6 +1,17 @@ #include "sentry_unwinder.h" #include "sentry_boot.h" +#if defined(SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE) \ + || defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE) +# include "sentry_logger.h" + +# include +# include +# include +# include +# include +#endif + #define DEFINE_UNWINDER(Func) \ size_t sentry__unwind_stack_##Func(void *addr, \ const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) @@ -38,6 +49,7 @@ void sentry__unwind_init_libunwindstack(void); } while (0) DEFINE_THREAD_UNWINDER(libunwind_remote); +DEFINE_THREAD_UNWINDER(libunwindstack_remote); #endif static size_t @@ -90,9 +102,63 @@ sentry__unwind_stack_from_thread(pid_t tid, sentry_remote_frame_t *frames, (void)symbolize; # ifdef SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE TRY_THREAD_UNWINDER(libunwind_remote); +# endif +# ifdef SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE + TRY_THREAD_UNWINDER(libunwindstack_remote); # endif return 0; } + +# if defined(SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE) \ + || defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE) +/** + * Attach to a thread of another process and wait until it stops. + * Returns false, leaving the thread detached, if either fails. + */ +bool +sentry__ptrace_attach_thread(pid_t tid) +{ + if (ptrace(PTRACE_ATTACH, tid, NULL, NULL) != 0) { + SENTRY_WARNF("remote_unwind: ptrace attach failed for %d: %s", tid, + strerror(errno)); + return false; + } + + // Poll with WNOHANG to avoid hanging on threads in D state. PTRACE_ATTACH + // sends SIGSTOP but the thread won't stop until it leaves uninterruptible + // sleep, which may never happen. + int status = 0; + int retries = 20; + for (;;) { + pid_t waited = waitpid(tid, &status, __WALL | WNOHANG); + if (waited == tid) { + break; + } + if (waited < 0) { + SENTRY_WARNF("remote_unwind: waitpid failed for %d: %s", tid, + strerror(errno)); + ptrace(PTRACE_DETACH, tid, NULL, NULL); + return false; + } + if (--retries <= 0) { + SENTRY_WARNF("remote_unwind: waitpid timed out for %d", tid); + ptrace(PTRACE_DETACH, tid, NULL, NULL); + return false; + } + usleep(100000); + } + + if (!WIFSTOPPED(status)) { + SENTRY_WARNF( + "remote_unwind: thread %d did not stop after attach: status=%d", + tid, status); + ptrace(PTRACE_DETACH, tid, NULL, NULL); + return false; + } + + return true; +} +# endif #endif void diff --git a/src/unwinder/sentry_unwinder.h b/src/unwinder/sentry_unwinder.h index 047ab6fc10..5023269e7c 100644 --- a/src/unwinder/sentry_unwinder.h +++ b/src/unwinder/sentry_unwinder.h @@ -38,6 +38,8 @@ size_t sentry__unwind_stack_from_thread(pid_t tid, sentry_remote_frame_t *frames, size_t max_frames, sentry_remote_registers_t *registers, bool symbolize); +bool sentry__ptrace_attach_thread(pid_t tid); + #endif /** diff --git a/src/unwinder/sentry_unwinder_libunwind_remote.c b/src/unwinder/sentry_unwinder_libunwind_remote.c index ca99aa7d53..1c1743841e 100644 --- a/src/unwinder/sentry_unwinder_libunwind_remote.c +++ b/src/unwinder/sentry_unwinder_libunwind_remote.c @@ -15,11 +15,8 @@ # include "sentry_logger.h" -# include # include # include -# include -# include # include # include @@ -78,41 +75,7 @@ sentry__unwind_stack_from_thread_libunwind_remote(pid_t tid, memset(registers, 0, sizeof(*registers)); } - if (ptrace(PTRACE_ATTACH, tid, NULL, NULL) != 0) { - SENTRY_WARNF("remote_unwind: ptrace attach failed for %d: %s", tid, - strerror(errno)); - return 0; - } - - // Poll with WNOHANG to avoid hanging on threads in D state. PTRACE_ATTACH - // sends SIGSTOP but the thread won't stop until it leaves uninterruptible - // sleep, which may never happen. - int status = 0; - int retries = 20; - for (;;) { - pid_t waited = waitpid(tid, &status, __WALL | WNOHANG); - if (waited == tid) { - break; - } - if (waited < 0) { - SENTRY_WARNF("remote_unwind: waitpid failed for %d: %s", tid, - strerror(errno)); - ptrace(PTRACE_DETACH, tid, NULL, NULL); - return 0; - } - if (--retries <= 0) { - SENTRY_WARNF("remote_unwind: waitpid timed out for %d", tid); - ptrace(PTRACE_DETACH, tid, NULL, NULL); - return 0; - } - usleep(100000); - } - - if (!WIFSTOPPED(status)) { - SENTRY_WARNF( - "remote_unwind: thread %d did not stop after attach: status=%d", - tid, status); - ptrace(PTRACE_DETACH, tid, NULL, NULL); + if (!sentry__ptrace_attach_thread(tid)) { return 0; } diff --git a/src/unwinder/sentry_unwinder_libunwindstack_remote.cpp b/src/unwinder/sentry_unwinder_libunwindstack_remote.cpp new file mode 100644 index 0000000000..93a09d1a9e --- /dev/null +++ b/src/unwinder/sentry_unwinder_libunwindstack_remote.cpp @@ -0,0 +1,120 @@ +extern "C" { +#include "sentry_boot.h" +#include "sentry_logger.h" +#include "sentry_unwinder.h" +} + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void +capture_registers(unwindstack::Regs *regs, sentry_remote_registers_t *registers) +{ + regs->IterateRegisters([registers](const char *name, uint64_t value) { + size_t len = strlen(name); + if (registers->count >= SENTRY_REMOTE_UNWIND_MAX_REGISTERS || len == 0 + || len >= sizeof(registers->values[0].name)) { + return; + } + sentry_remote_register_t *remote_register + = ®isters->values[registers->count++]; + memcpy(remote_register->name, name, len + 1); + remote_register->value = value; + }); +} + +static size_t +unwind_thread(pid_t tid, sentry_remote_frame_t *frames, size_t max_frames, + sentry_remote_registers_t *registers, bool symbolize) +{ + // an uncaught exception, e.g. std::bad_alloc, would abort the daemon + try { + std::unique_ptr regs( + unwindstack::Regs::RemoteGet(tid)); + if (!regs) { + SENTRY_WARNF("libunwindstack failed to read registers of %d", tid); + return 0; + } + if (registers) { + capture_registers(regs.get(), registers); + } + + unwindstack::RemoteMaps maps(tid); + if (!maps.Parse()) { + SENTRY_WARNF("libunwindstack failed to parse /proc/%d/maps", tid); + return 0; + } + + std::shared_ptr process_memory + = unwindstack::Memory::CreateProcessMemoryCached(tid); + if (!process_memory) { + SENTRY_WARNF( + "libunwindstack failed to read process memory for %d", tid); + return 0; + } + + unwindstack::Unwinder unwinder( + max_frames, &maps, regs.get(), process_memory); + unwinder.SetResolveNames(symbolize); + unwinder.Unwind(); + + if (unwinder.LastErrorCode() != unwindstack::ERROR_NONE) { + SENTRY_DEBUGF("libunwindstack finished with %s at 0x%llx", + unwinder.LastErrorCodeString(), + (unsigned long long)unwinder.LastErrorAddress()); + } + + size_t count = 0; + for (const unwindstack::FrameData &frame : unwinder.frames()) { + if (count >= max_frames) { + break; + } + frames[count].ip = frame.pc; + frames[count].symbol[0] = '\0'; + frames[count].symbol_offset = 0; + if (!frame.function_name.empty()) { + snprintf(frames[count].symbol, sizeof(frames[count].symbol), + "%s", frame.function_name.c_str()); + frames[count].symbol_offset = frame.function_offset; + } + count++; + } + + SENTRY_DEBUGF( + "libunwindstack remote unwind captured %zu frames", count); + return count; + } catch (...) { + SENTRY_WARN("libunwindstack failed with an exception"); + return 0; + } +} + +extern "C" size_t +sentry__unwind_stack_from_thread_libunwindstack_remote(pid_t tid, + sentry_remote_frame_t *frames, size_t max_frames, + sentry_remote_registers_t *registers, bool symbolize) +{ + if (registers) { + registers->count = 0; + } + + if (tid <= 0 || !frames || max_frames == 0) { + return 0; + } + + if (!sentry__ptrace_attach_thread(tid)) { + return 0; + } + + size_t count = unwind_thread(tid, frames, max_frames, registers, symbolize); + + ptrace(PTRACE_DETACH, tid, NULL, NULL); + return count; +} From f815b40f5ba89dea16a351ce13b0ee0fdc4f110e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Oct 2026 13:53:34 +0100 Subject: [PATCH 38/47] feat(native): support preloading the crash handler (#2268) * feat(native): Support ELF modules at non-zero file offsets The minidump writer and `capture_modules_from_proc_maps()` grouped `/proc//maps` entries by file name and expected an ELF header at offset 0. An ELF image can also start inside a larger file, such as an uncompressed library that Android maps straight from an APK, and such images were dropped from the module list. Start a module at each mapping whose file offset holds an ELF header, group the image's other segments by its load range, and read the build ID, size, and SONAME at that offset. Move `compute_elf_size_from_phdrs()` to `sentry_elf.h` to share it. This prepares the native backend for Android, where AGP's default packaging maps every app library from the APK. Ref: #1725 * fix(native): Apply the ELF offset to the build ID section headers The previous commit adjusted the section header offset in `elf_locate_symtab()` instead of `extract_elf_build_id_for_module()`, where `elf_offset` is not declared, so the crash daemon failed to compile. Move the adjustment to where the build ID is read. * fix(native): Keep embedded ELFs mapped below their file offset The `offset > start` guard in `capture_modules_from_proc_maps()` dropped mappings before an ELF image inside a larger file could be detected. The guard only protects the `start - offset` load base of a regular ELF file, so apply it there instead. * feat(unwinder): Unwind remote threads with libunwindstack The crash daemon could only unwind the threads that did not crash with `libunwind_remote`, which Android builds lack because they use libunwindstack. Add a `libunwindstack_remote` backend, compiled in only with the native backend, that attaches to a thread with ptrace, reads its registers with `Regs::RemoteGet()`, and unwinds it. Share the ptrace attach loop with `libunwind_remote`. This prepares the native backend for Android. * feat(native): Support preloading the crash handler `sentry__backend_preload()` was a no-op for the native backend. Install its signal handlers early, so that they keep their place in the signal chain ahead of a managed runtime, and let a later `sentry_init()` adopt them. Like with `inproc`, shutdown keeps the preloaded handlers installed, forwarding crashes to the previous handlers until the next initialization. --- src/backends/native/sentry_crash_handler.c | 48 ++++++++++++++++++++-- src/backends/native/sentry_crash_handler.h | 5 +++ src/backends/sentry_backend_native.c | 5 +++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index db8ce22919..f9a83e60c9 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -125,6 +125,8 @@ static sentry_handler_strategy_t g_handler_strategy = SENTRY_HANDLER_STRATEGY_DEFAULT; static struct sigaction g_previous_handlers[16]; static stack_t g_signal_stack = { 0 }; +static volatile long g_handlers_installed = 0; +static volatile long g_preloaded = 0; static void reset_signal_handlers(void) @@ -973,14 +975,22 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) reraise_signal(signum); } -int -sentry__crash_handler_init(sentry_crash_ipc_t *ipc, +static int +install_signal_handlers(sentry_crash_ipc_t *ipc, bool preload, sentry_handler_strategy_t strategy, void (*callback)(void)) { - if (!ipc && !callback) { + if (!preload && !ipc && !callback) { return -1; } + if (sentry__atomic_fetch(&g_handlers_installed)) { + if (!preload) { + g_crash_ipc = ipc; + g_handler_strategy = strategy; + } + return 0; + } + g_crash_ipc = ipc; g_handler_strategy = strategy; g_crash_callback = callback; @@ -1019,6 +1029,12 @@ sentry__crash_handler_init(sentry_crash_ipc_t *ipc, installed_count++; } + g_crash_ipc = ipc; + sentry__atomic_store(&g_handlers_installed, 1); + if (preload) { + sentry__atomic_store(&g_preloaded, 1); + } + // Prime libunwind's internal cache by performing a short unwind. // This forces dl_iterate_phdr to be called now (while safe) rather than // in the signal handler where it could deadlock if the crash occurs @@ -1052,6 +1068,20 @@ sentry__crash_handler_init(sentry_crash_ipc_t *ipc, return 0; } +int +sentry__crash_handler_init(sentry_crash_ipc_t *ipc, + sentry_handler_strategy_t strategy, void (*callback)(void)) +{ + return install_signal_handlers(ipc, false, strategy, callback); +} + +int +sentry__crash_handler_preload(void) +{ + return install_signal_handlers( + NULL, true, SENTRY_HANDLER_STRATEGY_DEFAULT, NULL); +} + int sentry__crash_handler_reinstall(void) { @@ -1087,6 +1117,16 @@ sentry__crash_handler_reinstall(void) void sentry__crash_handler_shutdown(void) { + if (sentry__atomic_fetch(&g_preloaded)) { + g_crash_ipc = NULL; + SENTRY_DEBUG("crash handler deactivated, keeping preloaded handlers"); + return; + } + + if (!sentry__atomic_fetch(&g_handlers_installed)) { + return; + } + // Restore previous signal handlers reset_signal_handlers(); @@ -1095,6 +1135,8 @@ sentry__crash_handler_shutdown(void) g_crash_ipc = NULL; g_crash_callback = NULL; + sentry__atomic_store(&g_handlers_installed, 0); + sentry__atomic_store(&g_preloaded, 0); SENTRY_DEBUG("crash handler shutdown"); } diff --git a/src/backends/native/sentry_crash_handler.h b/src/backends/native/sentry_crash_handler.h index a2c12054bc..cf409f9969 100644 --- a/src/backends/native/sentry_crash_handler.h +++ b/src/backends/native/sentry_crash_handler.h @@ -15,6 +15,11 @@ int sentry__crash_handler_init(sentry_crash_ipc_t *ipc, */ int sentry__crash_handler_reinstall(void); +/** + * Preload crash handler before full initialization. + */ +int sentry__crash_handler_preload(void); + /** * Shutdown crash handler (restore previous handlers) */ diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index d33021b480..601ae7deee 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -1663,6 +1663,11 @@ native_backend_reinstall(sentry_backend_t *backend) void sentry__backend_preload(void) { +#ifdef SENTRY_PLATFORM_UNIX + if (sentry__crash_handler_preload() < 0) { + SENTRY_WARN("failed to preload native crash handler"); + } +#endif } /** From a4a98f3aecf78be97b195f4b0f556fba40d569fc Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Oct 2026 14:16:34 +0100 Subject: [PATCH 39/47] fix(native): Consume the preload state when a signal falls through Before `sentry_init()` and after `sentry_close()`, preloaded handlers are only a placeholder in the signal chain. A signal that reached them restored the previous handlers but left `g_handlers_installed` and `g_preloaded` set, so a later `sentry_init()` took the early return and never reinstalled the crash handler. The fall-through also re-raised the signal after the previous handler returned, killing the process even if a handler installed before the preload had recovered from it. Like the inproc backend, consume the preload state before forwarding the signal, and return to wherever the previous handler left off instead of re-raising. Re-raise only when the signal had its default action before the preload. --- src/backends/native/sentry_crash_handler.c | 29 ++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index f9a83e60c9..8bb1a0b134 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -143,7 +143,7 @@ reraise_signal(int signum) raise(signum); } -static void +static bool invoke_previous_signal_handler(int signum, siginfo_t *info, void *context) { for (size_t i = 0; i < g_crash_signal_count; i++) { @@ -152,8 +152,11 @@ invoke_previous_signal_handler(int signum, siginfo_t *info, void *context) } struct sigaction *handler = &g_previous_handlers[i]; - if (handler->sa_handler == SIG_DFL || handler->sa_handler == SIG_IGN) { - return; + if (handler->sa_handler == SIG_DFL) { + return false; + } + if (handler->sa_handler == SIG_IGN) { + return true; } if (handler->sa_flags & SA_SIGINFO) { @@ -162,8 +165,9 @@ invoke_previous_signal_handler(int signum, siginfo_t *info, void *context) handler->sa_handler(signum); } - return; + return true; } + return false; } /** @@ -426,13 +430,24 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) sentry_crash_ipc_t *ipc = g_crash_ipc; 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 + // Re-enable previous signal handlers to prevent loops reset_signal_handlers(); + bool preloaded = sentry__atomic_fetch(&g_preloaded); + if (preloaded) { + // Consume the preload so that a later init reinstalls the handlers + sentry__atomic_store(&g_handlers_installed, 0); + sentry__atomic_store(&g_preloaded, 0); + } + bool handled = false; if (g_handler_strategy != SENTRY_HANDLER_STRATEGY_CHAIN_AT_START) { - invoke_previous_signal_handler(signum, info, context); + handled = invoke_previous_signal_handler(signum, info, context); } - // The previous handler returned, fall back to default termination sentry__exit_crash_handler(); + if (preloaded && handled) { + // Resume wherever the previous handler left off + return; + } + // The previous handler returned, fall back to default termination reraise_signal(signum); return; } From bfdd59adeb15d1a4d5f0e1a4954949f7151e3f47 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Oct 2026 15:00:44 +0100 Subject: [PATCH 40/47] fix(native): Skip stack reads for threads without registers Threads enumerated from `/proc//task` have no saved registers, so the daemon called `process_vm_readv` at stack address 0 for each of them. The read always failed and logged before the remote unwinder took over. Skip the read when the stack pointer is unknown, and include `errno` in the remaining `process_vm_readv` and thread enumeration failures. --- src/backends/native/sentry_crash_daemon.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 118b8fa9a7..88ab1b37e9 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -898,7 +898,8 @@ build_stacktrace_for_thread( } #elif defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) // On Linux, use process_vm_readv to read stack memory from crashed process - if (ctx->platform.num_threads > 0) { + // (threads enumerated from /proc have no registers) + if (ctx->platform.num_threads > 0 && sp != 0) { pid_t pid = ctx->crashed_pid; stack_size = SENTRY_CRASH_MAX_STACK_CAPTURE; stack_start = sp; @@ -911,8 +912,9 @@ build_stacktrace_for_thread( 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"); + SENTRY_DEBUGF("process_vm_readv failed for %d: %s, falling " + "back to single frame", + pid, strerror(errno)); sentry_free(stack_buf); stack_buf = NULL; } else { @@ -1927,7 +1929,8 @@ enumerate_threads_from_proc(sentry_crash_context_t *ctx) DIR *dir = opendir(task_path); if (!dir) { - SENTRY_WARNF("Failed to open %s for thread enumeration", task_path); + SENTRY_WARNF("Failed to open %s for thread enumeration: %s", task_path, + strerror(errno)); return; } From f15b5e6cf39a044cc7d46e321d3c48b06ca54cb9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Oct 2026 16:43:39 +0100 Subject: [PATCH 41/47] feat(native): Support the chain-at-start handler strategy on Android (#2273) Allow `SENTRY_HANDLER_STRATEGY_CHAIN_AT_START` in the native backend on Android, as in the inproc backend, so that a signal that Mono turns into a managed exception, such as a `NullReferenceException`, is not captured as a native crash. As in the inproc backend, fall back to the default strategy below API 26, where debuggerd kills the process after the chained handler runs. This prepares the native backend for Android support (#1725). --- include/sentry.h | 4 ++-- src/backends/native/sentry_crash_handler.c | 2 +- src/backends/sentry_backend_native.c | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index c3ecc8494f..d9231a51d4 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -3510,8 +3510,8 @@ SENTRY_API sentry_metrics_result_t sentry_scope_capture_metric( /** * Returns the currently set strategy for the handler. * - * This option works for the `inproc` backend on `Linux` and `Android`, and for - * the `native` backend on `Linux`. + * This option works for the `inproc` and `native` backends on `Linux` and + * `Android`. * * The main use-case is when the Native SDK is used in the context of the * CLR/Mono runtimes which convert some POSIX signals into managed-code diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index 8bb1a0b134..56995b2ecd 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -407,7 +407,7 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) sentry__enter_crash_handler(); ucontext_t *uctx = (ucontext_t *)context; -# if defined(SENTRY_PLATFORM_LINUX) && !defined(SENTRY_PLATFORM_ANDROID) +# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) if (g_handler_strategy == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START && signum != SIGABRT && uctx) { uintptr_t ip = sentry__ucontext_get_ip(uctx); diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 601ae7deee..0479dbc54e 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -15,6 +15,9 @@ # include # include # endif +# if defined(SENTRY_PLATFORM_ANDROID) +# include +# endif #endif #include @@ -1006,10 +1009,20 @@ native_backend_startup( } sentry_handler_strategy_t strategy = -# if defined(SENTRY_PLATFORM_LINUX) && !defined(SENTRY_PLATFORM_ANDROID) +# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) options ? sentry_options_get_handler_strategy(options) : # endif SENTRY_HANDLER_STRATEGY_DEFAULT; +# if defined(SENTRY_PLATFORM_ANDROID) + // CHAIN_AT_START invokes the previous handler and expects to regain + // control. On Android API < 26, the old debuggerd daemon kills the + // crashing process via SIGKILL after the chained handler triggers + // it, so we fall back to DEFAULT which chains at the end instead. + if (strategy == SENTRY_HANDLER_STRATEGY_CHAIN_AT_START + && android_get_device_api_level() < 26) { + strategy = SENTRY_HANDLER_STRATEGY_DEFAULT; + } +# endif if (sentry__crash_handler_init(state->ipc, strategy, NULL) < 0) { SENTRY_WARN("failed to initialize crash handler"); # if defined(SENTRY_PLATFORM_UNIX) From 76ed3a8c44484badb233b81e347cf3ee19e26d28 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 29 Oct 2026 17:35:01 +0100 Subject: [PATCH 42/47] feat(linux): RISC-V support (#2232) * feat(linux): RISC-V support Support register capture, stack unwinding, ELF metadata, and native minidump generation. * Update CHANGELOG.md * context.version * llvm * setup-qemu-action for riscv64 * tweak skip conditions * gcc < 14 * ci: restore qemu-user for Arm32 * clarify & restore original phrasing --- .github/workflows/ci.yml | 31 +++++++++++-- CHANGELOG.md | 1 + README.md | 8 ++-- .../native/minidump/sentry_minidump_common.c | 2 + .../native/minidump/sentry_minidump_format.h | 13 ++++++ .../native/minidump/sentry_minidump_linux.c | 45 ++++++++++++++++-- src/backends/native/sentry_crash_daemon.c | 37 ++++++++++++--- src/backends/native/sentry_crash_handler.c | 11 ++++- src/sentry_elf.h | 18 +++++--- src/sentry_os.c | 11 +++++ src/sentry_os.h | 4 ++ tests/build_config.py | 10 ++++ tests/conditions.py | 1 + tests/requirements.txt | 2 +- tests/test_dotnet_signals.py | 5 +- tests/test_integration_crashpad.py | 12 ++++- tests/test_integration_native.py | 4 ++ tests/unit/test_native_backend.c | 10 +++- vendor/libunwind/CMakeLists.txt | 46 ++++++++++++++++++- vendor/libunwind/VENDORING.md | 7 +-- vendor/stb_sprintf.c | 3 ++ 21 files changed, 250 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abbd51da4b..e7b482e737 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,12 +69,18 @@ jobs: CC: gcc-9 CXX: g++-9 TEST_X86: 1 - - name: Linux Arm32 (gcc-arm-linux-gnueabihf + qemu-user) + - name: Linux Arm32 (cross-GCC + QEMU) os: ubuntu-24.04-arm CC: arm-linux-gnueabihf-gcc CXX: arm-linux-gnueabihf-g++ TEST_ARM32: 1 TEST_QEMU: 1 + - name: Linux RISC-V (cross-GCC + QEMU) + os: ubuntu-24.04-arm + CC: riscv64-linux-gnu-gcc + CXX: riscv64-linux-gnu-g++ + TEST_RISCV: 1 + TEST_QEMU: 1 - name: Linux (GCC 9.5.0) os: ubuntu-24.04 CC: gcc-9 @@ -260,6 +266,7 @@ jobs: TEST_MINGW: ${{ matrix.TEST_MINGW }} TEST_WINE: ${{ matrix.TEST_WINE }} TEST_ARM32: ${{ matrix.TEST_ARM32 }} + TEST_RISCV: ${{ matrix.TEST_RISCV }} TEST_QEMU: ${{ matrix.TEST_QEMU }} TEST_SYSTEM_PROXY: ${{ matrix.TEST_SYSTEM_PROXY }} USE_SCCACHE: ${{ matrix.USE_SCCACHE }} @@ -297,7 +304,7 @@ jobs: [ -n "$CC" ] && [ -n "$CXX" ] || { echo "Ubuntu runner configurations require toolchain selection via CC and CXX" >&2; exit 1; } - name: Installing Linux Dependencies - if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !env['TEST_ARM32'] && !env['ANDROID_API'] && !matrix.container }} + if: ${{ runner.os == 'Linux' && !env['TEST_X86'] && !env['TEST_ARM32'] && !env['TEST_RISCV'] && !env['ANDROID_API'] && !matrix.container }} run: | sudo apt update # Install common dependencies @@ -368,11 +375,29 @@ jobs: libstdc++6:armhf - name: Installing qemu-user - if: ${{ runner.os == 'Linux' && env['TEST_QEMU'] && !matrix.container }} + if: ${{ runner.os == 'Linux' && env['TEST_ARM32'] && !matrix.container }} run: | sudo apt update sudo apt install -y qemu-user-static binfmt-support + - name: Installing Linux RISC-V Dependencies + if: ${{ runner.os == 'Linux' && env['TEST_RISCV'] && !matrix.container }} + run: | + sudo dpkg --add-architecture riscv64 + sudo apt-get update + sudo apt-get install -y \ + cmake \ + gcc-riscv64-linux-gnu g++-riscv64-linux-gnu \ + zlib1g-dev:riscv64 libssl-dev:riscv64 \ + libcurl4-openssl-dev:riscv64 liblzma-dev:riscv64 \ + libstdc++6:riscv64 + echo "PKG_CONFIG_LIBDIR=/usr/lib/riscv64-linux-gnu/pkgconfig:/usr/share/pkgconfig" >> "$GITHUB_ENV" + + - uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + if: ${{ runner.os == 'Linux' && env['TEST_RISCV'] && !matrix.container }} + with: + platforms: riscv64 + - name: Installing Alpine Linux Dependencies if: ${{ contains(matrix.container, 'alpine') }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8a6409c6..aa8f7ea84a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - 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)) +- Linux: add RISC-V 64 support. ([#2232](https://github.com/getsentry/sentry-native/pull/2232)) - Report the host OS in the OS context under Wine and Proton, instead of the Windows version that Wine emulates. On Linux, prefer the host `os-release` inside Steam Runtime and Flatpak containers, and report SteamOS and Bazzite as the OS name. ([#2241](https://github.com/getsentry/sentry-native/pull/2241)) - Add `sentry_scope_set_trace` for per-scope traces. ([#2236](https://github.com/getsentry/sentry-native/pull/2236)) - Add `sentry_continue_trace` to continue incoming traces without a transaction. ([#2237](https://github.com/getsentry/sentry-native/pull/2237)) diff --git a/README.md b/README.md index 798b60c8a9..3ad98fa43a 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The SDK bundle contains the following folders: The SDK currently supports and is tested on the following OS/Compiler variations: -- Linux (x86, x64, arm32 and arm64) with GCC 9+ and Clang 11+ +- Linux (x86, x64 and arm64) with GCC 9+ and Clang 11+ - Windows (x86, x64 and arm64) with MSVC (Visual Studio 2022+) and LLVM-MinGW - macOS 15+ with Apple Clang and LLVM Clang 15+ - Android API 21+ with NDK 23+ @@ -80,9 +80,11 @@ The SDK currently supports and is tested on the following OS/Compiler variations - Nintendo Switch via [sentry-switch](https://github.com/getsentry/sentry-switch). See [Nintendo Switch documentation](https://docs.sentry.io/platforms/nintendo-switch/) to get access. Additionally, the SDK should support the following platforms, although they are -not automatically tested, so breakage may occur: +not fully tested, so breakage may occur: -- Windows Versions lower than Windows 10 / Windows Server 2016 +- Windows versions lower than Windows 10 / Windows Server 2016: not tested in CI. +- Linux arm32 and riscv64: cross-compiled in CI, with only a subset of non-crash + tests run under QEMU. The SDK supports different features on the target platform: diff --git a/src/backends/native/minidump/sentry_minidump_common.c b/src/backends/native/minidump/sentry_minidump_common.c index 84fb4d0bc6..fa8b507699 100644 --- a/src/backends/native/minidump/sentry_minidump_common.c +++ b/src/backends/native/minidump/sentry_minidump_common.c @@ -213,6 +213,8 @@ sentry__minidump_get_context_size(void) return sizeof(minidump_context_x86_t); # elif defined(__arm__) return sizeof(minidump_context_arm_t); +# elif defined(__riscv) && __riscv_xlen == 64 + return sizeof(minidump_context_riscv64_t); # else # error "Unsupported architecture" # endif diff --git a/src/backends/native/minidump/sentry_minidump_format.h b/src/backends/native/minidump/sentry_minidump_format.h index 6daf02f360..9770c5b7e8 100644 --- a/src/backends/native/minidump/sentry_minidump_format.h +++ b/src/backends/native/minidump/sentry_minidump_format.h @@ -50,6 +50,7 @@ typedef enum { MINIDUMP_CPU_ARM = 5, // PROCESSOR_ARCHITECTURE_ARM MINIDUMP_CPU_X86_64 = 9, // PROCESSOR_ARCHITECTURE_AMD64 MINIDUMP_CPU_ARM64 = 12, // PROCESSOR_ARCHITECTURE_ARM64 + MINIDUMP_CPU_RISCV64 = 0x8006, // MD_CPU_ARCHITECTURE_RISCV64 (Breakpad) } minidump_cpu_type_t; // OS types (must match Breakpad MDOSPlatform values) @@ -318,6 +319,18 @@ typedef struct { uint32_t fpextra[8]; } PACKED_ATTR minidump_context_arm_t; PACKED_STRUCT_END + +#elif defined(__riscv) && __riscv_xlen == 64 +PACKED_STRUCT_BEGIN +typedef struct { + uint32_t context_flags; + uint32_t version; + uint64_t regs[32]; // PC, X1-X31 + uint64_t fpregs[32]; // F0-F31 + uint32_t fcsr; + uint32_t reserved; +} PACKED_ATTR minidump_context_riscv64_t; +PACKED_STRUCT_END #endif /** diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index 25c314ea16..42ce54f368 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -259,6 +259,18 @@ ptrace_get_thread_registers(pid_t tid, ucontext_t *uctx) SENTRY_DEBUGF("ptrace(PTRACE_GETREGSET) failed for thread %d: %s", tid, strerror(errno)); } +# elif defined(__riscv) && __riscv_xlen == 64 + struct iovec iov + = { uctx->uc_mcontext.__gregs, sizeof(uctx->uc_mcontext.__gregs) }; + if (ptrace(PTRACE_GETREGSET, tid, (void *)NT_PRSTATUS, &iov) == 0) { + success = true; + struct iovec fp_iov = { &uctx->uc_mcontext.__fpregs.__d, + sizeof(uctx->uc_mcontext.__fpregs.__d) }; + ptrace(PTRACE_GETREGSET, tid, (void *)NT_FPREGSET, &fp_iov); + } else { + SENTRY_DEBUGF("ptrace(PTRACE_GETREGSET) failed for thread %d: %s", tid, + strerror(errno)); + } # elif defined(__i386__) struct user_regs_struct regs; if (ptrace(PTRACE_GETREGS, tid, NULL, ®s) == 0) { @@ -548,6 +560,8 @@ write_system_info_stream(minidump_writer_t *writer, minidump_directory_t *dir) sysinfo.processor_architecture = MINIDUMP_CPU_X86; # elif defined(__arm__) sysinfo.processor_architecture = MINIDUMP_CPU_ARM; +# elif defined(__riscv) && __riscv_xlen == 64 + sysinfo.processor_architecture = MINIDUMP_CPU_RISCV64; # endif # if defined(SENTRY_PLATFORM_ANDROID) @@ -856,6 +870,19 @@ write_thread_context( return write_data(writer, &context, sizeof(context)); +# elif defined(__riscv) && __riscv_xlen == 64 + (void)tid; + + minidump_context_riscv64_t context = { 0 }; + context.context_flags = 0x08000003; // RISCV64 | Integer | Floating point + context.version = 1; + memcpy(context.regs, uctx->uc_mcontext.__gregs, sizeof(context.regs)); + memcpy(context.fpregs, uctx->uc_mcontext.__fpregs.__d.__f, + sizeof(context.fpregs)); + context.fcsr = uctx->uc_mcontext.__fpregs.__d.__fcsr; + + return write_data(writer, &context, sizeof(context)); + # else # error "Unsupported architecture for Linux" # endif @@ -875,7 +902,8 @@ extract_elf_build_id(const char *elf_path, uint64_t elf_offset, } // Read ELF header -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) Elf64_Ehdr ehdr; # else Elf32_Ehdr ehdr; @@ -915,7 +943,8 @@ extract_elf_build_id(const char *elf_path, uint64_t elf_offset, return 0; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) Elf64_Shdr *sections = (Elf64_Shdr *)shdr_buf; # else Elf32_Shdr *sections = (Elf32_Shdr *)shdr_buf; @@ -976,7 +1005,8 @@ read_elf_soname(const char *elf_path, uint64_t elf_offset, char *soname_buf, return false; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) Elf64_Ehdr ehdr; typedef Elf64_Dyn DynT; typedef Elf64_Shdr ShdrT; @@ -1313,6 +1343,8 @@ ptrace_capture_thread( ptrace_sp = (uint32_t)ptrace_ctx.uc_mcontext.gregs[REG_ESP]; # elif defined(__arm__) ptrace_sp = ptrace_ctx.uc_mcontext.arm_sp; +# elif defined(__riscv) && __riscv_xlen == 64 + ptrace_sp = ptrace_ctx.uc_mcontext.__gregs[REG_SP]; # endif if (ptrace_sp != 0) { @@ -1400,6 +1432,8 @@ write_thread_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) sp = (uint32_t)uctx->uc_mcontext.gregs[REG_ESP]; # elif defined(__arm__) sp = uctx->uc_mcontext.arm_sp; +# elif defined(__riscv) && __riscv_xlen == 64 + sp = uctx->uc_mcontext.__gregs[REG_SP]; # endif SENTRY_DEBUGF("Thread %u: has context, SP=0x%llx", @@ -2089,6 +2123,11 @@ walk_indirect_registers_for_tid(minidump_writer_t *writer, pid_t tid, sentry__indirect_consider(acc, base, (uint64_t)(uint32_t)uctx->uc_mcontext.gregs[x86_gprs[i]], ops); } +# elif defined(__riscv) && __riscv_xlen == 64 + for (int r = 1; r < 32; r++) { + sentry__indirect_consider( + acc, base, (uint64_t)uctx->uc_mcontext.__gregs[r], ops); + } # elif defined(__arm__) // ARMv7's mcontext fields are individual struct members rather than // an array; walk them explicitly. Most heap pointers we care about diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 88ab1b37e9..be7317a899 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -778,6 +778,10 @@ build_stacktrace_for_thread( ip = (uint64_t)thread_context->uc_mcontext.arm_pc; fp = (uint64_t)thread_context->uc_mcontext.arm_fp; sp = (uint64_t)thread_context->uc_mcontext.arm_sp; +# elif defined(__riscv) && __riscv_xlen == 64 + ip = (uint64_t)thread_context->uc_mcontext.__gregs[REG_PC]; + fp = (uint64_t)thread_context->uc_mcontext.__gregs[REG_S0]; + sp = (uint64_t)thread_context->uc_mcontext.__gregs[REG_SP]; # endif #elif defined(SENTRY_PLATFORM_MACOS) # if defined(__x86_64__) @@ -1180,6 +1184,21 @@ build_stacktrace_for_thread( break; } } +#elif defined(__riscv) && __riscv_xlen == 64 + // RISC-V frame records precede the CFA held in s0 + if (!read_stack_value(stack_buf, stack_start, stack_size, + current_fp - 2 * sizeof(uintptr_t), &saved_fp) + || !read_stack_value(stack_buf, stack_start, stack_size, + current_fp - sizeof(uintptr_t), &return_addr)) { + break; + } + // GCC < 14 leaf frames save only s0, so the caller is in ra + if (walk_count == 0 && return_addr >= stack_start + && return_addr < stack_end) { + saved_fp = return_addr; + return_addr + = (uint64_t)thread_context->uc_mcontext.__gregs[REG_RA]; + } #else // Read saved frame pointer and return address // Frame layout: [FP] = saved FP, [FP + pointer size] = return addr @@ -1296,7 +1315,8 @@ extract_elf_build_id_for_module(const char *elf_path, uint64_t elf_offset, } // Read ELF header -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) Elf64_Ehdr ehdr; # else Elf32_Ehdr ehdr; @@ -1334,7 +1354,8 @@ extract_elf_build_id_for_module(const char *elf_path, uint64_t elf_offset, return 0; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) Elf64_Shdr *sections = (Elf64_Shdr *)shdr_buf; # else Elf32_Shdr *sections = (Elf32_Shdr *)shdr_buf; @@ -1430,7 +1451,8 @@ elf_locate_symtab(const char *path, int allow_dynsym, sym_source_t *src, return 0; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) Elf64_Ehdr ehdr; # else Elf32_Ehdr ehdr; @@ -1461,7 +1483,8 @@ elf_locate_symtab(const char *path, int allow_dynsym, sym_source_t *src, return 0; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) Elf64_Shdr *sections = (Elf64_Shdr *)shdr_buf; # else Elf32_Shdr *sections = (Elf32_Shdr *)shdr_buf; @@ -1664,7 +1687,8 @@ sym_source_lookup(const sym_source_t *src, uint64_t sym_target, char *name_out, return 0; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) Elf64_Sym chunk[256]; # else Elf32_Sym chunk[256]; @@ -1688,7 +1712,8 @@ sym_source_lookup(const sym_source_t *src, uint64_t sym_target, char *name_out, } for (size_t k = 0; k < n; k++) { -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) unsigned char sym_type = ELF64_ST_TYPE(chunk[k].st_info); # else unsigned char sym_type = ELF32_ST_TYPE(chunk[k].st_info); diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index 56995b2ecd..43b5e16656 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -330,6 +330,8 @@ capture_frame_pointer_stack( fp = (uintptr_t)uctx->uc_mcontext.gregs[REG_EBP]; # elif defined(__aarch64__) fp = (uintptr_t)uctx->uc_mcontext.regs[29]; +# elif defined(__riscv) && __riscv_xlen == 64 + fp = (uintptr_t)uctx->uc_mcontext.__gregs[REG_S0]; # endif capture->frame_pointer = true; while (fp && fp >= sp && fp - sp <= SENTRY_CRASH_MAX_STACK_CAPTURE @@ -337,7 +339,14 @@ capture_frame_pointer_stack( && capture->count < SENTRY_CRASH_MAX_BACKTRACE_FRAMES) { uintptr_t pair[2]; struct iovec local = { pair, sizeof(pair) }; - struct iovec remote = { (void *)fp, sizeof(pair) }; + uintptr_t record = fp; +# if defined(__riscv) && __riscv_xlen == 64 + record -= sizeof(pair); + if (record < sp) { + break; + } +# endif + struct iovec remote = { (void *)record, sizeof(pair) }; if (syscall(SYS_process_vm_readv, getpid(), &local, 1, &remote, 1, 0) != (ssize_t)sizeof(pair)) { break; diff --git a/src/sentry_elf.h b/src/sentry_elf.h index 9c161bce0d..3e1d4176ab 100644 --- a/src/sentry_elf.h +++ b/src/sentry_elf.h @@ -64,7 +64,8 @@ sentry__elf_is_file(const char *path) static inline bool sentry__elf_is_native_class(const unsigned char e_ident[EI_NIDENT]) { -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) return e_ident[EI_CLASS] == ELFCLASS64; # else return e_ident[EI_CLASS] == ELFCLASS32; @@ -79,7 +80,8 @@ sentry__elf_has_shdr_size( return false; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) return e_shentsize == sizeof(Elf64_Shdr); # else return e_shentsize == sizeof(Elf32_Shdr); @@ -94,7 +96,8 @@ sentry__elf_has_sym_entsize( return false; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) return sh_entsize == sizeof(Elf64_Sym); # else return sh_entsize == sizeof(Elf32_Sym); @@ -109,7 +112,8 @@ sentry__elf_has_phdr_size( return false; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) return e_phentsize == sizeof(Elf64_Phdr); # else return e_phentsize == sizeof(Elf32_Phdr); @@ -227,7 +231,8 @@ sentry__elf_compute_size_from_phdrs(const char *elf_path, uint64_t elf_offset) return 0; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) Elf64_Ehdr ehdr; # else Elf32_Ehdr ehdr; @@ -260,7 +265,8 @@ sentry__elf_compute_size_from_phdrs(const char *elf_path, uint64_t elf_offset) return 0; } -# if defined(__x86_64__) || defined(__aarch64__) +# if defined(__x86_64__) || defined(__aarch64__) \ + || (defined(__riscv) && __riscv_xlen == 64) Elf64_Phdr *phdrs = (Elf64_Phdr *)phdr_buf; # else Elf32_Phdr *phdrs = (Elf32_Phdr *)phdr_buf; diff --git a/src/sentry_os.c b/src/sentry_os.c index 5248d42ec6..231540283a 100644 --- a/src/sentry_os.c +++ b/src/sentry_os.c @@ -1187,6 +1187,17 @@ sentry__build_registers(const sentry_ucontext_t *uctx) sentry_value_set_by_key(registers, "cpsr", sentry__value_new_addr(uctx->user_context->uc_mcontext.arm_cpsr)); +# elif defined(__riscv) && __riscv_xlen == 64 + + static const char *names[] + = { "pc", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0", + "a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5", + "s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6" }; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + sentry_value_set_by_key(registers, names[i], + sentry__value_new_addr(uctx->user_context->uc_mcontext.__gregs[i])); + } + # endif # undef SET_REG diff --git a/src/sentry_os.h b/src/sentry_os.h index 90bce1c847..aa5fe55fe0 100644 --- a/src/sentry_os.h +++ b/src/sentry_os.h @@ -26,6 +26,8 @@ sentry__ucontext_get_ip(const ucontext_t *uctx) return (uintptr_t)uctx->uc_mcontext.arm_pc; # elif defined(__aarch64__) return (uintptr_t)uctx->uc_mcontext.pc; +# elif defined(__riscv) && __riscv_xlen == 64 + return (uintptr_t)uctx->uc_mcontext.__gregs[REG_PC]; # else (void)uctx; return 0; @@ -43,6 +45,8 @@ sentry__ucontext_get_sp(const ucontext_t *uctx) return (uintptr_t)uctx->uc_mcontext.arm_sp; # elif defined(__aarch64__) return (uintptr_t)uctx->uc_mcontext.sp; +# elif defined(__riscv) && __riscv_xlen == 64 + return (uintptr_t)uctx->uc_mcontext.__gregs[REG_SP]; # else (void)uctx; return 0; diff --git a/tests/build_config.py b/tests/build_config.py index b0ecec3f3f..727e459a0b 100644 --- a/tests/build_config.py +++ b/tests/build_config.py @@ -62,6 +62,16 @@ def get_platform_cmake_args(): "-DCMAKE_ASM_COMPILER=arm-linux-gnueabihf-gcc", ] ) + elif sys.platform == "linux" and os.environ.get("TEST_RISCV"): + args.extend( + [ + "-DCMAKE_SYSTEM_NAME=Linux", + "-DCMAKE_SYSTEM_PROCESSOR=riscv64", + "-DCMAKE_C_COMPILER=riscv64-linux-gnu-gcc", + "-DCMAKE_CXX_COMPILER=riscv64-linux-gnu-g++", + "-DCMAKE_ASM_COMPILER=riscv64-linux-gnu-gcc", + ] + ) elif sys.platform == "linux" and os.environ.get("TEST_WINE"): cc = os.environ.get("CC") or "clang" cxx = os.environ.get("CXX") or "clang++" diff --git a/tests/conditions.py b/tests/conditions.py index 04e21514fc..2c0bd567ee 100644 --- a/tests/conditions.py +++ b/tests/conditions.py @@ -7,6 +7,7 @@ is_wine = bool(os.environ.get("TEST_WINE")) is_x86 = os.environ.get("TEST_X86") is_arm32 = bool(os.environ.get("TEST_ARM32")) +is_riscv = bool(os.environ.get("TEST_RISCV")) is_qemu = bool(os.environ.get("TEST_QEMU")) # opt-in because the tests change the user's system proxy settings (with sudo on macOS) has_system_proxy = bool(os.environ.get("TEST_SYSTEM_PROXY")) diff --git a/tests/requirements.txt b/tests/requirements.txt index 4559a2527b..7e0e5ddc44 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -4,7 +4,7 @@ pytest-httpserver==1.0.10 flaky==3.8.1 msgpack==1.2.3 pytest-xdist==3.8.0 -clang-format==20.1.5 +clang-format==20.1.5; platform_machine != "riscv64" pywin32==311; sys_platform == "win32" psutil==7.1.1 # For E2E tests that call Sentry API diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index 196dfcec77..0de2ae33c9 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -13,6 +13,8 @@ has_native, is_android, is_arm32, + is_qemu, + is_riscv, is_tsan, is_wine, is_x86, @@ -22,7 +24,8 @@ project_fixture_path = pathlib.Path("tests/fixtures/dotnet_signal") pytestmark = pytest.mark.skipif( - is_wine, reason=".NET signal tests do not support Windows targets under Wine" + is_wine or (is_riscv and is_qemu), + reason=".NET signal tests do not support Wine or emulated RISC-V targets", ) diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index 0af07c0f6d..805177b795 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -24,7 +24,14 @@ REPLAY_ID, SENTRY_VERSION, ) -from .conditions import has_crashpad, has_files, has_oom, has_system_proxy +from .conditions import ( + has_crashpad, + has_files, + has_oom, + has_system_proxy, + is_qemu, + is_riscv, +) from .proxy import ( setup_proxy_env_vars, cleanup_proxy_env_vars, @@ -48,8 +55,9 @@ wait_for_file, ) +# ptrace is unavailable under RISC-V user-mode emulation pytestmark = pytest.mark.skipif( - not has_crashpad, + not has_crashpad or (is_riscv and is_qemu), reason="Tests need a crashpad backend and a valid environment for it", ) diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 8ff820d2e4..2aa36c8ab0 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -773,6 +773,7 @@ def _parse_minidump(path): is_arm64 = cpu_arch in (0x8003, 0xC) is_amd64 = cpu_arch == 9 is_x86 = cpu_arch == 0 + is_riscv = cpu_arch == 0x8006 threads = [] if 3 in streams: # ThreadList @@ -799,6 +800,9 @@ def _parse_minidump(path): elif is_x86: # CONTEXT_X86: esp at offset 0xC4. sp = struct.unpack_from("/` and `include/tdep-/` directories from the diff --git a/vendor/stb_sprintf.c b/vendor/stb_sprintf.c index d60a91bae9..6cbd0f5ec1 100644 --- a/vendor/stb_sprintf.c +++ b/vendor/stb_sprintf.c @@ -1,2 +1,5 @@ +#include + +#define stbsp__uintptr uintptr_t #define STB_SPRINTF_IMPLEMENTATION #include "stb_sprintf.h" From 79c649475fa91e275e250546153d59f9617b079e Mon Sep 17 00:00:00 2001 From: Nizan Mandelblit <138482993+nmandelblit@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:18:25 +0100 Subject: [PATCH 43/47] feat: Add pkg-config support (#1411) * feat: Add pkg-config support - Added support for generating and installing a .pc file for pkg-config - Controlled by the SENTRY_INSTALL_PKGCONFIG CMake option - Uses CMAKE_INSTALL_LIBDIR and CMAKE_INSTALL_INCLUDEDIR to support non-standard layouts (e.g., lib64) - Installs the generated file to the standard pkg-config directory * feat(pkgconfig): Expose the installed binary directory Export `bindir` in `sentry.pc` so consumers can locate the installed crash handler. Honor `CMAKE_INSTALL_BINDIR` when installing the native handler and its Windows companion so the metadata matches the layout. Add a standalone consumer that links through pkg-config and initializes the default backend using the installed handler and a writable database directory. * copy what inproc_stress_test does * fix(pkgconfig): Install sentry.pc only for shared builds A static `libsentry` needs its full private link graph, including vendored backend archives, system libraries and the C++ runtime. The CMake package already exports that graph. Install `sentry.pc` only for shared builds, where `-lsentry` is complete. --------- Co-authored-by: J-P Nurmi --- CHANGELOG.md | 1 + CMakeLists.txt | 36 +++++++++++++++++-- sentry.pc.in | 12 +++++++ tests/cmake.py | 13 ++++--- tests/fixtures/pkgconfig/CMakeLists.txt | 37 +++++++++++++++++++ tests/fixtures/pkgconfig/main.c | 20 +++++++++++ tests/test_build.py | 48 ++++++++++++++++++++++++- 7 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 sentry.pc.in create mode 100644 tests/fixtures/pkgconfig/CMakeLists.txt create mode 100644 tests/fixtures/pkgconfig/main.c diff --git a/CHANGELOG.md b/CHANGELOG.md index aa8f7ea84a..89a0aac09d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,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)) +- Add pkg-config support for shared builds. ([#1411](https://github.com/getsentry/sentry-native/pull/1411)) - 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)) - Linux: add RISC-V 64 support. ([#2232](https://github.com/getsentry/sentry-native/pull/2232)) - Report the host OS in the OS context under Wine and Proton, instead of the Windows version that Wine emulates. On Linux, prefer the host `os-release` inside Steam Runtime and Flatpak containers, and report SteamOS and Bazzite as the OS name. ([#2241](https://github.com/getsentry/sentry-native/pull/2241)) diff --git a/CMakeLists.txt b/CMakeLists.txt index b6d1e85715..fa07e8e456 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -246,6 +246,7 @@ if("${CMAKE_SOURCE_DIR}" STREQUAL "${PROJECT_SOURCE_DIR}") endif() option(SENTRY_ENABLE_INSTALL "Enable sentry installation" "${SENTRY_MAIN_PROJECT}") +option(SENTRY_INSTALL_PKGCONFIG "Generate and install pkg-config file for shared builds" ON) if(MSVC AND CMAKE_GENERATOR_TOOLSET MATCHES "_xp$") message(WARNING "Crashpad is not supported for MSVC with XP toolset. Default backend was switched to 'breakpad'") @@ -1038,11 +1039,11 @@ elseif(SENTRY_BACKEND_NATIVE) # Install daemon install(TARGETS sentry-crash - RUNTIME DESTINATION bin + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" ) if(WIN32) install(TARGETS sentry-wer - RUNTIME DESTINATION bin + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" ) @@ -1214,3 +1215,34 @@ if(SENTRY_BUILD_SHARED_LIBS) "$<$:-Wl,-z,max-page-size=16384>" ) endif() + +# Generate and install pkg-config file +if(SENTRY_INSTALL_PKGCONFIG AND SENTRY_BUILD_SHARED_LIBS) + set(PREFIX "${CMAKE_INSTALL_PREFIX}") + set(PKGCONFIG_VERSION "${SENTRY_VERSION_FULL}") + # Use absolute paths for libdir, includedir, and bindir to support non-standard layouts (e.g., lib64) + if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") + set(PKGCONFIG_LIBDIR "${CMAKE_INSTALL_LIBDIR}") + else() + set(PKGCONFIG_LIBDIR "\${prefix}/${CMAKE_INSTALL_LIBDIR}") + endif() + if(IS_ABSOLUTE "${CMAKE_INSTALL_INCLUDEDIR}") + set(PKGCONFIG_INCLUDEDIR "${CMAKE_INSTALL_INCLUDEDIR}") + else() + set(PKGCONFIG_INCLUDEDIR "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") + endif() + if(IS_ABSOLUTE "${CMAKE_INSTALL_BINDIR}") + set(PKGCONFIG_BINDIR "${CMAKE_INSTALL_BINDIR}") + else() + set(PKGCONFIG_BINDIR "\${prefix}/${CMAKE_INSTALL_BINDIR}") + endif() + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/sentry.pc.in" + "${CMAKE_CURRENT_BINARY_DIR}/sentry.pc" + @ONLY + ) + sentry_install( + FILES "${CMAKE_CURRENT_BINARY_DIR}/sentry.pc" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig" + ) +endif() diff --git a/sentry.pc.in b/sentry.pc.in new file mode 100644 index 0000000000..0de461e287 --- /dev/null +++ b/sentry.pc.in @@ -0,0 +1,12 @@ +prefix=@PREFIX@ +exec_prefix=${prefix} +libdir=@PKGCONFIG_LIBDIR@ +includedir=@PKGCONFIG_INCLUDEDIR@ +bindir=@PKGCONFIG_BINDIR@ + +Name: sentry +Description: Sentry SDK for C, C++ and native applications +Version: @PKGCONFIG_VERSION@ +URL: https://sentry.io/ +Libs: -L${libdir} -lsentry +Cflags: -I${includedir} diff --git a/tests/cmake.py b/tests/cmake.py index 0da3d40532..1eb7aef0cc 100644 --- a/tests/cmake.py +++ b/tests/cmake.py @@ -23,7 +23,7 @@ def __init__(self, factory): self.runs = dict() self.factory = factory - def compile(self, targets, options=None, cflags=None): + def compile(self, targets, options=None, cflags=None, source_dir=None): # We build in tmp for all of our tests. Disable the warning MSVC generates to not clutter the build logs. if cflags is None: cflags = [] @@ -35,11 +35,12 @@ def compile(self, targets, options=None, cflags=None): key = ";".join(f"{k}={v}" for k, v in options.items()) if cflags: key += ";" + ";".join(cflags) + key = (source_dir, key) # cache the build configuration if key not in self.runs: cwd = self.factory.mktemp("cmake") - cmake_configure(cwd, options, cflags) + cmake_configure(cwd, options, cflags, source_dir) cmake_build(cwd, targets, options) self.runs[key] = (cwd, set(targets)) else: @@ -97,6 +98,7 @@ def destroy(self): exe_name("sentry-crash"), exe_name("sentry_early_init"), exe_name("sentry_test_platform"), + exe_name("sentry_test_pkgconfig"), ] cmd = [ os.environ.get("LLVM_COV", "llvm-cov"), @@ -129,7 +131,7 @@ def destroy(self): ) -def cmake_configure(cwd, options, cflags=None): +def cmake_configure(cwd, options, cflags=None, source_dir=None): if cflags is None: cflags = [] __tracebackhide__ = True @@ -166,7 +168,8 @@ def cmake_configure(cwd, options, cflags=None): options.update(get_android_config()) - source_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + if source_dir is None: + source_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) cmake = ["cmake"] if "scan-build" in os.environ.get("RUN_ANALYZER", ""): @@ -206,7 +209,7 @@ def cmake_configure(cwd, options, cflags=None): env = dict(os.environ) env["CFLAGS"] = env["CXXFLAGS"] = " ".join(cflags) - config_cmd.append(source_dir) + config_cmd.append(os.fspath(source_dir)) print("\n{} > {}".format(cwd, " ".join(config_cmd)), flush=True) try: diff --git a/tests/fixtures/pkgconfig/CMakeLists.txt b/tests/fixtures/pkgconfig/CMakeLists.txt new file mode 100644 index 0000000000..d7b2a340a3 --- /dev/null +++ b/tests/fixtures/pkgconfig/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.10) +project(sentry_pkgconfig C) + +# Options that match the main sentry-native build +option(SENTRY_BUILD_FORCE32 "Force a 32bit compile on a 64bit host" OFF) +option(WITH_ASAN_OPTION "Build with address sanitizer" OFF) +option(WITH_TSAN_OPTION "Build with thread sanitizer" OFF) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(SENTRY REQUIRED IMPORTED_TARGET sentry) + +pkg_get_variable(SENTRY_BINDIR sentry bindir) +find_program(SENTRY_HANDLER_PATH sentry-crash + PATHS "${SENTRY_BINDIR}" + NO_DEFAULT_PATH ONLY_CMAKE_FIND_ROOT_PATH) + +add_executable(sentry_test_pkgconfig main.c) +target_link_libraries(sentry_test_pkgconfig PRIVATE PkgConfig::SENTRY) +target_compile_definitions(sentry_test_pkgconfig PRIVATE + SENTRY_HANDLER_PATH=\"${SENTRY_HANDLER_PATH}\") + +if(NOT MSVC) + if(SENTRY_BUILD_FORCE32) + target_compile_options(sentry_test_pkgconfig PRIVATE -m32) + target_link_libraries(sentry_test_pkgconfig PRIVATE -m32) + endif() + + if(WITH_ASAN_OPTION) + target_compile_options(sentry_test_pkgconfig PRIVATE -fsanitize=address) + target_link_libraries(sentry_test_pkgconfig PRIVATE -fsanitize=address) + endif() + + if(WITH_TSAN_OPTION) + target_compile_options(sentry_test_pkgconfig PRIVATE -fsanitize=thread) + target_link_libraries(sentry_test_pkgconfig PRIVATE -fsanitize=thread) + endif() +endif() diff --git a/tests/fixtures/pkgconfig/main.c b/tests/fixtures/pkgconfig/main.c new file mode 100644 index 0000000000..933d085519 --- /dev/null +++ b/tests/fixtures/pkgconfig/main.c @@ -0,0 +1,20 @@ +#include +#include + +int +main(void) +{ + const char *database = getenv("SENTRY_DATABASE_PATH"); + if (!database) { + return 1; + } + + sentry_options_t *options = sentry_options_new(); + sentry_options_set_handler_path(options, SENTRY_HANDLER_PATH); + sentry_options_set_database_path(options, database); + if (sentry_init(options) != 0) { + return 1; + } + sentry_close(); + return 0; +} diff --git a/tests/test_build.py b/tests/test_build.py index 956a90789d..15fa22a2da 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -2,10 +2,14 @@ import sys import os import struct +import shutil +import shlex +from pathlib import Path import pytest -from . import SENTRY_VERSION, lib_name +from . import SENTRY_VERSION, lib_name, run +from .build_config import get_test_executable_env from .conditions import ( has_breakpad, has_crashpad, @@ -193,3 +197,45 @@ def test_static_mpack(cmake): assert any( symbol.startswith("sentry__mpack_") for symbol in symbols ), f"{library.name} contains no Sentry-prefixed MPack symbols" + + +@pytest.mark.skipif( + sys.platform not in ("linux", "darwin") or not has_native or is_qemu or is_wine, + reason="test requires a native Unix build", +) +def test_pkgconfig(cmake, tmp_path, monkeypatch): + if shutil.which("pkg-config") is None: + pytest.skip("pkg-config is not installed") + + destdir = tmp_path / "install" + directory = cmake(["sentry"], {"SENTRY_TRANSPORT": "none"}) + subprocess.run( + ["cmake", "--install", directory], + env=dict(os.environ, DESTDIR=str(destdir)), + check=True, + ) + + (pc,) = destdir.rglob("sentry.pc") + (library,) = destdir.rglob(lib_name("sentry")) + _assert_shared_version(library.parent) + + monkeypatch.setenv("PKG_CONFIG_PATH", str(pc.parent)) + monkeypatch.setenv("PKG_CONFIG_LIBDIR", str(pc.parent)) + monkeypatch.setenv("PKG_CONFIG_SYSROOT_DIR", str(destdir)) + env = get_test_executable_env() + version = subprocess.check_output( + ["pkg-config", "--modversion", "sentry"], env=env, text=True + ) + assert version.strip() == SENTRY_VERSION + flags = subprocess.check_output( + ["pkg-config", "--libs-only-L", "sentry"], env=env, text=True + ) + assert shlex.split(flags) == [f"-L{library.parent}"] + + consumer = cmake( + ["sentry_test_pkgconfig"], + {"CMAKE_FIND_ROOT_PATH": str(destdir)}, + source_dir=Path(__file__).parent / "fixtures" / "pkgconfig", + ) + env["SENTRY_DATABASE_PATH"] = str(tmp_path / "database") + run(consumer, "sentry_test_pkgconfig", [], env=env) From 341a1c29cce21de30067736425e83285a8a2f61f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 30 Oct 2026 07:55:47 +0100 Subject: [PATCH 44/47] test: Skip the pkg-config test on Android Running the native backend tests on Android also enabled `test_pkgconfig`, which expects a desktop install. Android builds have no versioned `libsentry.so` symlinks, macOS hosts look for a `.dylib`, and the consumer would have to run on the device. Skip it like `test_shared_lib`. --- tests/test_build.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_build.py b/tests/test_build.py index 15fa22a2da..2a79187934 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -200,7 +200,11 @@ def test_static_mpack(cmake): @pytest.mark.skipif( - sys.platform not in ("linux", "darwin") or not has_native or is_qemu or is_wine, + sys.platform not in ("linux", "darwin") + or not has_native + or is_android + or is_qemu + or is_wine, reason="test requires a native Unix build", ) def test_pkgconfig(cmake, tmp_path, monkeypatch): From 602bdfa2e133b6d85e8db306c3c70cea9063852b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Nov 2026 14:41:55 +0100 Subject: [PATCH 45/47] feat(tus): resumable uploads (#2058) * feat(tus): support Upload-Offset Query persisted TUS uploads for their current offset and send only the remaining bytes from file-backed request bodies. https://tus.io/protocols/resumable-upload#upload-offset * fix(tus): honor rate limits on upload responses Only envelope responses updated the rate limiter, so `Retry-After` and `X-Sentry-Rate-Limits` from the upload endpoint were ignored. Move the update into `http_send_request` so that every response feeds it. A 429 on an upload request also dropped the attachment with a `send_error` client report, although Relay records outcomes for 429 responses. Skip the report in that case. * fix(tus): retry uploads on 503 and 504 A 503 or 504 on a TUS request dropped the attachment and its persisted upload location, although the upload spec marks both as retryable. Treat them like 409, so that the envelope is kept for retry and the next attempt resumes from the offset reported by `HEAD`. --- CHANGELOG.md | 1 + include/sentry.h | 21 +- src/path/sentry_path_unix.c | 25 + src/path/sentry_path_windows.c | 21 + src/sentry_envelope.c | 26 +- src/sentry_envelope.h | 5 +- src/sentry_path.h | 7 + src/transports/sentry_http_transport.c | 412 ++++++++++---- src/transports/sentry_http_transport.h | 11 +- src/transports/sentry_http_transport_curl.c | 13 +- .../sentry_http_transport_winhttp.c | 67 ++- tests/fixtures/integration.c | 5 +- tests/proxy.py | 8 +- tests/proxy_server.py | 52 +- tests/test_e2e_sentry.py | 115 +++- tests/test_integration_tus.py | 536 ++++++++++++++++-- tests/test_proxy_server.py | 47 +- tests/unit/test_envelopes.c | 33 ++ tests/unit/test_http_transport.c | 36 ++ tests/unit/test_tus.c | 144 ++++- tests/unit/tests.inc | 3 + 21 files changed, 1385 insertions(+), 203 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89a0aac09d..8a04062eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Add `sentry_scope_set_trace` for per-scope traces. ([#2236](https://github.com/getsentry/sentry-native/pull/2236)) - Add `sentry_continue_trace` to continue incoming traces without a transaction. ([#2237](https://github.com/getsentry/sentry-native/pull/2237)) - Add `sentry_iter_trace_propagation_headers` to propagate traces without a span. ([#2240](https://github.com/getsentry/sentry-native/pull/2240)) +- Resume interrupted TUS attachment uploads from the server-provided upload offset. ([#2058](https://github.com/getsentry/sentry-native/pull/2058)) **Fixes**: diff --git a/include/sentry.h b/include/sentry.h index d9231a51d4..dbfa9d92ed 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1148,7 +1148,8 @@ SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_body( * Returns the path to a file that should be streamed as the request body of * `req`, and writes its size to `*len_out`. Returns `NULL` unless `req` * requires a file-backed body -- see `sentry_http_request_get_body`, which - * also covers requests with no body at all. + * also covers requests with no body at all. Custom HTTP clients must honor + * `sentry_http_request_get_body_file_offset` when streaming the file. * * The path is in a platform-specific filesystem path encoding, which on * Windows is UTF-8 rather than the ANSI code page the narrow Win32 APIs @@ -1159,6 +1160,15 @@ SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_body( SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_body_file_path( const sentry_http_request_t *req, size_t *len_out); +/** + * Returns the byte offset at which a file-backed request body starts. The + * client must seek to this position before streaming the number of bytes + * returned by `sentry_http_request_get_body_file_path`. Returns `0` for + * requests without a file-backed body. + */ +SENTRY_EXPERIMENTAL_API size_t sentry_http_request_get_body_file_offset( + const sentry_http_request_t *req); + #ifdef SENTRY_PLATFORM_WINDOWS /** * Wide char version of `sentry_http_request_get_body_file_path`. @@ -1185,10 +1195,11 @@ SENTRY_EXPERIMENTAL_API void sentry_http_response_set_status_code( /** * Records a response header on `resp`. `key` is matched case-insensitively * against the headers sentry-native cares about (currently `Retry-After`, - * `X-Sentry-Rate-Limits`, and `Location`); anything else is ignored. Pass - * every header the HTTP response actually had -- sentry-native, not the - * client, decides which ones matter, so headers Sentry starts caring about - * later don't require client changes. + * `X-Sentry-Rate-Limits`, `Location`, `Upload-Offset`, `Upload-Min-Chunk-Size`, + * `Upload-Max-Chunk-Size`, and `Upload-Chunk-Granularity`); anything else is + * ignored. Pass every header the HTTP response actually had -- sentry-native, + * not the client, decides which ones matter, so headers Sentry starts caring + * about later don't require client changes. */ SENTRY_EXPERIMENTAL_API void sentry_http_response_set_header( sentry_http_response_t *resp, const char *key, const char *value); diff --git a/src/path/sentry_path_unix.c b/src/path/sentry_path_unix.c index 28540a6f54..f5ee9c1a63 100644 --- a/src/path/sentry_path_unix.c +++ b/src/path/sentry_path_unix.c @@ -704,3 +704,28 @@ sentry__filewriter_byte_count(const sentry_filewriter_t *filewriter) { return filewriter ? filewriter->byte_count : 0; } + +FILE * +sentry__path_open(const sentry_path_t *path, const char *mode, size_t offset) +{ + if (!path || !mode) { + return NULL; + } + FILE *file = fopen(path->path, mode); + if (!file) { + return NULL; + } +#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) + int result = offset > LONG_MAX ? -1 : fseek(file, (long)offset, SEEK_SET); +#else + off_t file_offset = (off_t)offset; + int result = file_offset < 0 || (size_t)file_offset != offset + ? -1 + : fseeko(file, file_offset, SEEK_SET); +#endif + if (result != 0) { + fclose(file); + return NULL; + } + return file; +} diff --git a/src/path/sentry_path_windows.c b/src/path/sentry_path_windows.c index 7169a89afd..4350b32923 100644 --- a/src/path/sentry_path_windows.c +++ b/src/path/sentry_path_windows.c @@ -870,3 +870,24 @@ sentry__filewriter_byte_count(const sentry_filewriter_t *filewriter) { return filewriter ? filewriter->byte_count : 0; } + +FILE * +sentry__path_open(const sentry_path_t *path, const char *mode, size_t offset) +{ + if (!path || !mode) { + return NULL; + } + wchar_t *mode_w = sentry__string_to_wstr(mode); + FILE *file = mode_w ? _wfopen(path->path_w, mode_w) : NULL; + sentry_free(mode_w); + if (!file) { + return NULL; + } + int result + = offset > INT64_MAX ? -1 : _fseeki64(file, (int64_t)offset, SEEK_SET); + if (result != 0) { + fclose(file); + return NULL; + } + return file; +} diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 98012713bc..d3458d7620 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -668,6 +668,10 @@ attachment_ref_payload_to_string( sentry_value_set_by_key( obj, "content_type", sentry_value_new_string(ref->content_type)); } + if (ref->chunk_size) { + sentry_value_set_by_key(obj, "chunk_size", + sentry_value_new_uint64((uint64_t)ref->chunk_size)); + } sentry__jsonwriter_write_value(jw, obj); sentry_value_decref(obj); return sentry__jsonwriter_into_string(jw, payload_len); @@ -1587,6 +1591,19 @@ sentry__envelope_item_get_attachment_ref( sentry_value_get_by_key(ref->_owner, "content_type")); ref->attachment_type = sentry_value_as_string( sentry_value_get_by_key(item->headers, "attachment_type")); + sentry_value_t value = sentry_value_get_by_key(ref->_owner, "chunk_size"); + uint64_t chunk_size = 0; + if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_UINT64) { + chunk_size = sentry_value_as_uint64(value); + } else { + int64_t size = sentry_value_as_int64(value); + if (size > 0) { + chunk_size = (uint64_t)size; + } + } + if (chunk_size <= SIZE_MAX) { + ref->chunk_size = (size_t)chunk_size; + } return true; } @@ -1621,8 +1638,8 @@ set_attachment_ref_payload( } bool -sentry__envelope_item_resolve_attachment_ref( - sentry_envelope_item_t *item, const char *location) +sentry__envelope_item_resolve_attachment_ref(sentry_envelope_item_t *item, + const char *path, const char *location, size_t chunk_size) { if (sentry__string_empty(location)) { return false; @@ -1632,13 +1649,14 @@ sentry__envelope_item_resolve_attachment_ref( return false; } sentry_attachment_ref_t resolved = { 0 }; - if (!sentry__string_empty(ref.path)) { - resolved.path = ref.path; + if (!sentry__string_empty(path)) { + resolved.path = path; } if (!sentry__string_empty(ref.attachment_type)) { resolved.attachment_type = ref.attachment_type; } resolved.location = location; + resolved.chunk_size = chunk_size; if (!sentry__string_empty(ref.content_type)) { resolved.content_type = ref.content_type; } diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index 1e8f16ee6a..1aa1681b20 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -23,6 +23,7 @@ typedef struct { const char *location; const char *content_type; const char *attachment_type; + size_t chunk_size; sentry_value_t _owner; } sentry_attachment_ref_t; @@ -223,8 +224,8 @@ void sentry__attachment_ref_cleanup(sentry_attachment_ref_t *ref); /** * Resolves an attachment-ref item with a remote TUS location. */ -bool sentry__envelope_item_resolve_attachment_ref( - sentry_envelope_item_t *item, const char *location); +bool sentry__envelope_item_resolve_attachment_ref(sentry_envelope_item_t *item, + const char *path, const char *location, size_t chunk_size); /** * If `envelope` is raw, parse it in place into a structured envelope. diff --git a/src/sentry_path.h b/src/sentry_path.h index 2dd2cf2d25..d6a638b644 100644 --- a/src/sentry_path.h +++ b/src/sentry_path.h @@ -4,6 +4,7 @@ #include "sentry_boot.h" #include "sentry_string.h" +#include #include struct sentry_path_s { @@ -220,6 +221,12 @@ int sentry__path_touch(const sentry_path_t *path); */ size_t sentry__path_get_size(const sentry_path_t *path); +/** + * Opens `path` with `mode` and seeks to `offset`, or returns NULL on failure. + */ +FILE *sentry__path_open( + const sentry_path_t *path, const char *mode, size_t offset); + /** * This will return the last modification time of the file at `path`, or 0 on * failure. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 99ab08d1fa..61aa358828 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -21,6 +21,7 @@ #define ENVELOPE_MIME "application/x-sentry-envelope" #define TUS_MIME "application/offset+octet-stream" +#define TUS_CHUNK_SIZE (32 * 1024 * 1024) // 32 MiB #define TUS_MAX_HTTP_HEADERS 4 #ifdef SENTRY_TRANSPORT_COMPRESSION # define MAX_HTTP_HEADERS 4 @@ -270,20 +271,22 @@ prepare_tus_request_common(size_t upload_size, const char *attachment_type, h->key = "upload-length"; h->value = sentry__uint64_to_string((uint64_t)upload_size); - if (!sentry__string_empty(attachment_type)) { - h = &req->headers[req->headers_len++]; - h->key = "upload-metadata"; - h->value = tus_build_upload_metadata(attachment_type); - } + h = &req->headers[req->headers_len++]; + h->key = "upload-metadata"; + h->value = tus_build_upload_metadata(sentry__string_empty(attachment_type) + ? SENTRY_ATTACHMENT_TYPE_GENERIC + : attachment_type); return req; } static sentry_prepared_http_request_t * prepare_tus_upload_request(const char *location, const sentry_path_t *path, - size_t file_size, const sentry_dsn_t *dsn, const char *user_agent) + size_t file_size, size_t upload_offset, size_t body_len, + const sentry_dsn_t *dsn, const char *user_agent) { - if (!location || !path) { + if (!location || !path || upload_offset > file_size + || body_len > file_size - upload_offset) { return NULL; } @@ -305,7 +308,8 @@ prepare_tus_upload_request(const char *location, const sentry_path_t *path, req->method = "PATCH"; req->url = sentry__string_clone(location); req->body_path = sentry__path_clone(path); - req->body_len = file_size; + req->body_len = body_len; + req->body_offset = upload_offset; sentry_prepared_http_header_t *h; h = &req->headers[req->headers_len++]; @@ -322,7 +326,43 @@ prepare_tus_upload_request(const char *location, const sentry_path_t *path, h = &req->headers[req->headers_len++]; h->key = "upload-offset"; - h->value = sentry__string_clone("0"); + h->value = sentry__uint64_to_string((uint64_t)upload_offset); + + return req; +} + +static sentry_prepared_http_request_t * +prepare_tus_offset_request( + const char *location, const sentry_dsn_t *dsn, const char *user_agent) +{ + if (!location) { + return NULL; + } + + sentry_prepared_http_request_t *req + = SENTRY_MAKE(sentry_prepared_http_request_t); + if (!req) { + return NULL; + } + memset(req, 0, sizeof(*req)); + + req->headers = sentry_malloc(sizeof(sentry_prepared_http_header_t) * 2); + if (!req->headers) { + sentry_free(req); + return NULL; + } + + req->method = "HEAD"; + req->url = sentry__string_clone(location); + + sentry_prepared_http_header_t *h; + h = &req->headers[req->headers_len++]; + h->key = "x-sentry-auth"; + h->value = sentry__dsn_get_auth_header(dsn, user_agent); + + h = &req->headers[req->headers_len++]; + h->key = "tus-resumable"; + h->value = sentry__string_clone("1.0.0"); return req; } @@ -333,6 +373,10 @@ http_response_cleanup(sentry_http_response_t *resp) sentry_free(resp->retry_after); sentry_free(resp->x_sentry_rate_limits); sentry_free(resp->location); + sentry_free(resp->upload_offset); + sentry_free(resp->upload_chunk_granularity); + sentry_free(resp->upload_min_chunk_size); + sentry_free(resp->upload_max_chunk_size); } void @@ -357,6 +401,18 @@ sentry_http_response_set_header( } else if (sentry__string_eq(lower_key, "location")) { sentry_free(resp->location); resp->location = sentry__string_clone(value); + } else if (sentry__string_eq(lower_key, "upload-offset")) { + sentry_free(resp->upload_offset); + resp->upload_offset = sentry__string_clone(value); + } else if (sentry__string_eq(lower_key, "upload-chunk-granularity")) { + sentry_free(resp->upload_chunk_granularity); + resp->upload_chunk_granularity = sentry__string_clone(value); + } else if (sentry__string_eq(lower_key, "upload-min-chunk-size")) { + sentry_free(resp->upload_min_chunk_size); + resp->upload_min_chunk_size = sentry__string_clone(value); + } else if (sentry__string_eq(lower_key, "upload-max-chunk-size")) { + sentry_free(resp->upload_max_chunk_size); + resp->upload_max_chunk_size = sentry__string_clone(value); } sentry_free(lower_key); } @@ -436,6 +492,12 @@ sentry_http_request_get_body_file_path( return req->body_path->path; } +size_t +sentry_http_request_get_body_file_offset(const sentry_http_request_t *req) +{ + return req && req->body_path ? req->body_offset : 0; +} + #ifdef SENTRY_PLATFORM_WINDOWS const wchar_t * sentry_http_request_get_body_file_pathw( @@ -458,23 +520,9 @@ enum { RESULT_OK = 0, RESULT_ERROR = -1, RESULT_SHUTDOWN = -2, + RESULT_DISCARD = -3, }; -static int -http_send_request(http_transport_state_t *state, - sentry_prepared_http_request_t *req, sentry_http_response_t *resp) -{ - memset(resp, 0, sizeof(*resp)); - if (!state->send_func(state->client, req, resp)) { - int result = sentry__atomic_fetch(&state->shutting_down) - ? RESULT_SHUTDOWN - : RESULT_ERROR; - http_response_cleanup(resp); - return result; - } - return resp->status_code; -} - static bool http_handle_error(int status_code) { @@ -502,88 +550,244 @@ http_update_ratelimiter( } else if (resp->status_code == 429) { sentry__rate_limiter_update_from_429(state->ratelimiter); } - http_response_cleanup(resp); +} + +static int +http_send_request(http_transport_state_t *state, + sentry_prepared_http_request_t *req, sentry_http_response_t *resp) +{ + memset(resp, 0, sizeof(*resp)); + if (!state->send_func(state->client, req, resp)) { + int result = sentry__atomic_fetch(&state->shutting_down) + ? RESULT_SHUTDOWN + : RESULT_ERROR; + http_response_cleanup(resp); + return result; + } + http_update_ratelimiter(state, resp); + return resp->status_code; +} + +static bool +tus_parse_size(const char *value, size_t max, size_t *size) +{ + if (sentry__string_empty(value) || !size) { + return false; + } + + size_t parsed = 0; + for (const char *p = value; *p; p++) { + if (*p < '0' || *p > '9') { + return false; + } + size_t digit = (size_t)(*p - '0'); + if (parsed > max / 10 || (parsed == max / 10 && digit > max % 10)) { + return false; + } + parsed = parsed * 10 + digit; + } + + *size = parsed; + return true; +} + +static bool +tus_choose_chunk_size(const sentry_http_response_t *resp, size_t *chunk_size) +{ + *chunk_size = 0; + if (!resp->upload_chunk_granularity || !resp->upload_min_chunk_size + || !resp->upload_max_chunk_size) { + return true; + } + + size_t granularity, min, max; + if (!tus_parse_size(resp->upload_chunk_granularity, SIZE_MAX, &granularity) + || !tus_parse_size(resp->upload_min_chunk_size, SIZE_MAX, &min) + || !tus_parse_size(resp->upload_max_chunk_size, SIZE_MAX, &max) + || !granularity || !min || min > max) { + return false; + } + + size_t size = TUS_CHUNK_SIZE; + if (size > max) { + size = max; + } + size -= size % granularity; + if (size < min) { + size_t rem = min % granularity; + if (rem && granularity - rem > max - min) { + return false; + } + size = rem ? min + granularity - rem : min; + } + *chunk_size = size; + return true; +} + +// The upload spec marks 503 and 504 as retryable. +static int +tus_status_result(int status) +{ + if (status == 503 || status == 504) { + return RESULT_ERROR; + } + return status ? status : RESULT_ERROR; } // Perform a TUS upload for the file at / and put the -// resulting remote location URL (caller frees) in `location_out`. +// resulting remote location into the attachment-ref item. static int tus_upload_file(http_transport_state_t *state, const sentry_path_t *cache_path, - const sentry_attachment_ref_t *ref, char **location_out) + sentry_envelope_item_t *item, const sentry_attachment_ref_t *ref) { - *location_out = NULL; sentry_path_t *att_file = sentry__path_join_filename(cache_path, ref->path); if (!att_file || !sentry__path_is_file(att_file) || sentry__path_is_symlink(att_file)) { sentry__path_free(att_file); - return RESULT_ERROR; + return RESULT_DISCARD; } size_t file_size = sentry__path_get_size(att_file); if (file_size == 0) { sentry__path_free(att_file); - return RESULT_ERROR; - } - - // Step 1: TUS creation (POST, no body) - sentry_prepared_http_request_t *req = prepare_tus_request_common( - file_size, ref->attachment_type, state->dsn, state->user_agent); - if (!req) { - sentry__path_free(att_file); - return RESULT_ERROR; + return RESULT_DISCARD; } + size_t upload_offset = 0; + size_t chunk_size = ref->chunk_size; + char *location = NULL; + sentry_prepared_http_request_t *req; sentry_http_response_t resp; - int status_code = http_send_request(state, req, &resp); - sentry__prepared_http_request_free(req); - if (status_code < 0) { - sentry__path_free(att_file); - return status_code; + bool create = sentry__string_empty(ref->location); + if (!create) { + // Step 1a: TUS upload offset (HEAD, no body) + char *upload_url = sentry__dsn_resolve_url(state->dsn, ref->location); + req = prepare_tus_offset_request( + upload_url, state->dsn, state->user_agent); + sentry_free(upload_url); + if (!req) { + sentry__path_free(att_file); + return RESULT_DISCARD; + } + + int status_code = http_send_request(state, req, &resp); + sentry__prepared_http_request_free(req); + if (status_code < 0) { + sentry__path_free(att_file); + return status_code; + } + + bool valid_offset = (resp.status_code == 200 || resp.status_code == 204) + && tus_parse_size(resp.upload_offset, file_size, &upload_offset); + int status = resp.status_code; + http_response_cleanup(&resp); + create = status == 404 || status == 410; + if (!create && !valid_offset) { + sentry__path_free(att_file); + return tus_status_result(status); + } + + if (!create) { + location = sentry__string_clone(ref->location); + } + if (!create && !location) { + sentry__path_free(att_file); + return RESULT_DISCARD; + } } + if (create) { + // Step 1b: TUS creation (POST, no body) + req = prepare_tus_request_common( + file_size, ref->attachment_type, state->dsn, state->user_agent); + if (!req) { + sentry__path_free(att_file); + return RESULT_DISCARD; + } - if (resp.status_code != 201 || !resp.location) { - sentry__path_free(att_file); + int status_code = http_send_request(state, req, &resp); + sentry__prepared_http_request_free(req); + if (status_code < 0) { + sentry__path_free(att_file); + return status_code; + } + + if (resp.status_code != 201 || sentry__string_empty(resp.location)) { + int status = resp.status_code; + sentry__path_free(att_file); + http_response_cleanup(&resp); + return tus_status_result(status); + } + + if (!tus_choose_chunk_size(&resp, &chunk_size)) { + sentry__path_free(att_file); + http_response_cleanup(&resp); + return RESULT_ERROR; + } + + // Record the upload URL so an interrupted PATCH can be resumed. + location = sentry__string_clone(resp.location); http_response_cleanup(&resp); - return RESULT_ERROR; + if (!location + || !sentry__envelope_item_resolve_attachment_ref( + item, ref->path, location, chunk_size)) { + sentry_free(location); + sentry__path_free(att_file); + return RESULT_DISCARD; + } } // Step 2: TUS upload (PATCH with file body). The placeholder needs the // raw `Location` value the TUS endpoint returned (relative path); only the // PATCH itself needs an absolute URL. - char *patch_url = sentry__dsn_resolve_url(state->dsn, resp.location); - char *location = sentry__string_clone(resp.location); - http_response_cleanup(&resp); - if (!location) { - sentry_free(patch_url); - sentry__path_free(att_file); - return RESULT_ERROR; - } - req = prepare_tus_upload_request( - patch_url, att_file, file_size, state->dsn, state->user_agent); - sentry_free(patch_url); - sentry__path_free(att_file); - if (!req) { - sentry_free(location); - return RESULT_ERROR; - } - - status_code = http_send_request(state, req, &resp); - sentry__prepared_http_request_free(req); - if (status_code < 0) { - sentry_free(location); - return status_code; - } - - int status = resp.status_code; - http_response_cleanup(&resp); + char *patch_url = sentry__dsn_resolve_url(state->dsn, location); + sentry_free(location); + int result = RESULT_OK; + // When resuming at the end of the file, an empty PATCH retrieves the + // final attachment Location for the placeholder. + do { + size_t len = file_size - upload_offset; + if (chunk_size && len > chunk_size) { + len = chunk_size; + } + req = prepare_tus_upload_request(patch_url, att_file, file_size, + upload_offset, len, state->dsn, state->user_agent); + if (!req) { + result = RESULT_DISCARD; + break; + } - if (status != 204) { - sentry_free(location); - return RESULT_ERROR; - } + int status = http_send_request(state, req, &resp); + sentry__prepared_http_request_free(req); + if (status < 0) { + result = status; + break; + } - *location_out = location; - return status; + size_t offset; + if (status == 409) { + result = RESULT_ERROR; + } else if (status != 204) { + result = tus_status_result(status); + } else if (!tus_parse_size(resp.upload_offset, file_size, &offset) + || offset < upload_offset || (offset == upload_offset && len) + || (offset == file_size && sentry__string_empty(resp.location))) { + result = RESULT_ERROR; + } else { + upload_offset = offset; + if (upload_offset == file_size + && !sentry__envelope_item_resolve_attachment_ref( + item, NULL, resp.location, 0)) { + result = RESULT_DISCARD; + } + } + http_response_cleanup(&resp); + if (result != RESULT_OK) { + break; + } + } while (upload_offset < file_size); + sentry_free(patch_url); + sentry__path_free(att_file); + return result; } // Collect the non-NULL `path` values from every attachment-ref item in the @@ -672,9 +876,10 @@ prune_attachment_refs(const sentry_run_t *run, sentry_value_t paths, } } -// Walk attachment-ref items: for each one with `path` and no `location`, try -// TUS upload and set `location`. If TUS is unavailable or fails for a given -// item, drop it and send the event without the large attachment. +// Walk attachment-ref items: upload new ones and resume previously interrupted +// ones. Local and HTTP errors drop the affected item; recoverable network +// errors and resumable uploads keep the envelope intact for transport retry, +// caching, or shutdown persistence. static int resolve_attachment_refs( http_transport_state_t *state, sentry_envelope_t *envelope) @@ -705,7 +910,8 @@ resolve_attachment_refs( continue; } - if (!sentry__string_empty(ref.location)) { + if (!sentry__string_empty(ref.location) + && sentry__string_empty(ref.path)) { // Crash-resume: TUS already done on a prior attempt. Nothing to do. sentry__attachment_ref_cleanup(&ref); i++; @@ -719,28 +925,25 @@ resolve_attachment_refs( continue; } - char *new_location = NULL; - int result = tus_upload_file(state, cache_path, &ref, &new_location); - if (new_location) { - bool resolved = sentry__envelope_item_resolve_attachment_ref( - item, new_location); - sentry_free(new_location); - if (resolved) { - sentry__attachment_ref_cleanup(&ref); - i++; - continue; - } + int result = tus_upload_file(state, cache_path, item, &ref); + if (result == RESULT_OK) { sentry__attachment_ref_cleanup(&ref); - return RESULT_ERROR; + i++; + continue; } - if (result == RESULT_SHUTDOWN) { + if (result == RESULT_SHUTDOWN + || (result == RESULT_ERROR + && (state->retry || state->cache_keep))) { sentry__attachment_ref_cleanup(&ref); return result; } sentry__attachment_ref_cleanup(&ref); - sentry__client_report_discard(SENTRY_DISCARD_REASON_SEND_ERROR, - SENTRY_DATA_CATEGORY_ATTACHMENT, 1); + // Relay records outcomes for 429 responses. + if (result != 429) { + sentry__client_report_discard(SENTRY_DISCARD_REASON_SEND_ERROR, + SENTRY_DATA_CATEGORY_ATTACHMENT, 1); + } if (!sentry__envelope_remove_item(envelope, item)) { return RESULT_ERROR; } @@ -785,7 +988,7 @@ http_send_envelope(sentry_envelope_t *envelope, void *_state) int status_code = http_send_request(state, req, &resp); sentry__prepared_http_request_free(req); if (status_code >= 0) { - http_update_ratelimiter(state, &resp); + http_response_cleanup(&resp); } return status_code; } @@ -1173,9 +1376,16 @@ sentry__prepare_tus_create_request(size_t file_size, sentry_prepared_http_request_t * sentry__prepare_tus_upload_request(const char *location, - const sentry_path_t *path, size_t file_size, const sentry_dsn_t *dsn, - const char *user_agent) + const sentry_path_t *path, size_t file_size, size_t upload_offset, + size_t body_len, const sentry_dsn_t *dsn, const char *user_agent) { return prepare_tus_upload_request( - location, path, file_size, dsn, user_agent); + location, path, file_size, upload_offset, body_len, dsn, user_agent); +} + +bool +sentry__tus_choose_chunk_size( + const sentry_http_response_t *resp, size_t *chunk_size) +{ + return tus_choose_chunk_size(resp, chunk_size); } diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index 0f2d11c66a..e9450b3aa0 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -19,6 +19,7 @@ typedef struct sentry_http_request_s { size_t headers_len; char *body; size_t body_len; + size_t body_offset; bool body_owned; sentry_path_t *body_path; } sentry_prepared_http_request_t; @@ -31,7 +32,8 @@ sentry_prepared_http_request_t *sentry__prepare_tus_create_request( const char *user_agent); sentry_prepared_http_request_t *sentry__prepare_tus_upload_request( const char *location, const sentry_path_t *path, size_t file_size, - const sentry_dsn_t *dsn, const char *user_agent); + size_t upload_offset, size_t body_len, const sentry_dsn_t *dsn, + const char *user_agent); void sentry__prepared_http_request_free(sentry_prepared_http_request_t *req); @@ -40,8 +42,15 @@ struct sentry_http_response_s { char *retry_after; char *x_sentry_rate_limits; char *location; + char *upload_offset; + char *upload_chunk_granularity; + char *upload_min_chunk_size; + char *upload_max_chunk_size; }; +bool sentry__tus_choose_chunk_size( + const sentry_http_response_t *resp, size_t *chunk_size); + /** * Creates a new HTTP transport with the given client and send function. * Use the setter functions below to configure optional client callbacks. diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 79c0846751..26f920cd69 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -470,14 +470,11 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, FILE *body_file = NULL; file_body_t file_body = { 0 }; if (req->body_path) { -#ifdef SENTRY_PLATFORM_WINDOWS - body_file = _wfopen(req->body_path->path_w, L"rb"); -#else - body_file = fopen(req->body_path->path, "rb"); -#endif + body_file = sentry__path_open(req->body_path, "rb", req->body_offset); if (!body_file) { - SENTRY_WARNF("failed to open request body file \"%s\"", - sentry__path_filename(req->body_path)); + SENTRY_WARNF("failed to open request body file \"%s\" at offset " + "%zu", + sentry__path_filename(req->body_path), req->body_offset); g_curl.slist_free_all(headers); return false; } @@ -493,6 +490,8 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, g_curl.easy_setopt(curl, CURLOPT_POST, (long)1); g_curl.easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); g_curl.easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); + } else if (sentry__string_eq(req->method, "HEAD")) { + g_curl.easy_setopt(curl, CURLOPT_NOBODY, 1L); } else { g_curl.easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method); } diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index a535ccd255..26b61cc173 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -204,18 +204,22 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, wchar_t *url = sentry__string_to_wstr(req->url); wchar_t *headers = NULL; + size_t url_len = url ? wcslen(url) : 0; + wchar_t *url_path = sentry_malloc((url_len + 1) * sizeof(wchar_t)); URL_COMPONENTS url_components; wchar_t hostname[128]; - wchar_t url_path[4096]; memset(&url_components, 0, sizeof(URL_COMPONENTS)); url_components.dwStructSize = sizeof(URL_COMPONENTS); url_components.lpszHostName = hostname; url_components.dwHostNameLength = 128; url_components.lpszUrlPath = url_path; - url_components.dwUrlPathLength = 1024; + url_components.dwUrlPathLength = (DWORD)(url_len + 1); - WinHttpCrackUrl(url, 0, 0, &url_components); + if (!url || !url_path || !WinHttpCrackUrl(url, 0, 0, &url_components)) { + SENTRY_WARNF("`WinHttpCrackUrl` failed with code `%d`", GetLastError()); + goto exit; + } #ifdef SENTRY_PLATFORM_XBOX // Ensure Xbox network connectivity is initialized before HTTP requests @@ -285,6 +289,22 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, goto exit; } + LARGE_INTEGER file_offset; + if (req->body_offset > INT64_MAX) { + SENTRY_WARNF("failed to seek request body file \"%s\"", + sentry__path_filename(req->body_path)); + CloseHandle(hFile); + goto exit; + } + file_offset.QuadPart = (LONGLONG)req->body_offset; + if (!SetFilePointerEx(hFile, file_offset, NULL, FILE_BEGIN)) { + SENTRY_WARNF( + "failed to seek request body file \"%s\" with code `%d`", + sentry__path_filename(req->body_path), GetLastError()); + CloseHandle(hFile); + goto exit; + } + // https://learn.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpsendrequest#support-for-greater-than-4-gb-upload DWORD total_length = req->body_len > (size_t)(DWORD)-1 ? WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH @@ -294,8 +314,11 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, if (result) { char chunk[65536]; DWORD bytes_read = 0; - while (true) { - if (!ReadFile(hFile, chunk, sizeof(chunk), &bytes_read, NULL)) { + size_t remaining = req->body_len; + while (remaining) { + DWORD len = remaining < sizeof(chunk) ? (DWORD)remaining + : sizeof(chunk); + if (!ReadFile(hFile, chunk, len, &bytes_read, NULL)) { SENTRY_WARNF("failed to read request body file \"%s\" " "with code `%d`", sentry__path_filename(req->body_path), GetLastError()); @@ -303,6 +326,7 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, break; } if (bytes_read == 0) { + result = false; break; } @@ -321,6 +345,7 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, result = false; break; } + remaining -= bytes_written; } } else { SENTRY_WARNF( @@ -398,6 +423,37 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, sentry_http_response_set_header(resp, "location", location); sentry_free(location); } + + char *upload_offset = query_header(client->request, L"upload-offset"); + if (upload_offset) { + sentry_http_response_set_header( + resp, "upload-offset", upload_offset); + sentry_free(upload_offset); + } + + char *upload_chunk_granularity + = query_header(client->request, L"upload-chunk-granularity"); + if (upload_chunk_granularity) { + sentry_http_response_set_header( + resp, "upload-chunk-granularity", upload_chunk_granularity); + sentry_free(upload_chunk_granularity); + } + + char *upload_min_chunk_size + = query_header(client->request, L"upload-min-chunk-size"); + if (upload_min_chunk_size) { + sentry_http_response_set_header( + resp, "upload-min-chunk-size", upload_min_chunk_size); + sentry_free(upload_min_chunk_size); + } + + char *upload_max_chunk_size + = query_header(client->request, L"upload-max-chunk-size"); + if (upload_max_chunk_size) { + sentry_http_response_set_header( + resp, "upload-max-chunk-size", upload_max_chunk_size); + sentry_free(upload_max_chunk_size); + } } uint64_t now = sentry__monotonic_time(); @@ -409,6 +465,7 @@ exit:; WinHttpCloseHandle(request); } sentry_free(url); + sentry_free(url_path); sentry_free(headers); return result; } diff --git a/tests/fixtures/integration.c b/tests/fixtures/integration.c index c41b63b196..584398f909 100644 --- a/tests/fixtures/integration.c +++ b/tests/fixtures/integration.c @@ -801,6 +801,9 @@ main(int argc, char **argv) if (has_arg(argc, argv, "log")) { sentry_options_set_debug(options, 1); } + if (has_arg(argc, argv, "trace")) { + sentry_options_set_logger_level(options, SENTRY_LEVEL_TRACE); + } if (has_arg(argc, argv, "attachment")) { // assuming the example / test is run directly from the cmake build @@ -1153,7 +1156,7 @@ main(int argc, char **argv) fclose(f); sentry_value_t attachment = sentry_attachment_from_file(large_file); sentry_attachment_set_type( - attachment, SENTRY_ATTACHMENT_TYPE_MINIDUMP); + attachment, SENTRY_ATTACHMENT_TYPE_GENERIC); sentry_add_attachment(attachment); } } diff --git a/tests/proxy.py b/tests/proxy.py index 155e527759..a30f4ec168 100644 --- a/tests/proxy.py +++ b/tests/proxy.py @@ -213,7 +213,11 @@ def _wait_for_output_file(process, output_file, timeout=10): def start_proxy( - proxy_type, proxy_auth: str = None, listen_host: str = "127.0.0.1", retries: int = 3 + proxy_type, + proxy_auth: str = None, + listen_host: str = "127.0.0.1", + retries: int = 3, + throttle: int = 0, ): """Start the stdlib test proxy on a free port. Returns (process, port).""" proxy_server = Path(__file__).with_name("proxy_server.py") @@ -242,6 +246,8 @@ def start_proxy( if proxy_auth: proxy_command += ["--proxy-auth", proxy_auth] + if throttle: + proxy_command += ["--throttle", str(throttle)] proxy_process = subprocess.Popen( proxy_command, diff --git a/tests/proxy_server.py b/tests/proxy_server.py index 78628372a1..d3a429ace8 100644 --- a/tests/proxy_server.py +++ b/tests/proxy_server.py @@ -6,6 +6,7 @@ import socket import sys import threading +import time from urllib.parse import urlsplit BUFFER_SIZE = 64 * 1024 @@ -163,7 +164,17 @@ def _read_http_response(server): return header + b"\r\n\r\n" + body, status_line -def _forward_http(client, proxy_auth): +def _sendall(sock, data, throttle): + if not throttle: + sock.sendall(data) + return + for offset in range(0, len(data), BUFFER_SIZE): + chunk = data[offset : offset + BUFFER_SIZE] + time.sleep(len(chunk) / throttle) + sock.sendall(chunk) + + +def _forward_http(client, proxy_auth, throttle): data = _recv_until(client, b"\r\n\r\n") header, initial_body = data.split(b"\r\n\r\n", 1) lines = header.decode("iso-8859-1").split("\r\n") @@ -175,6 +186,15 @@ def _forward_http(client, proxy_auth): _send_proxy_auth_required(client, method) return + if method == "CONNECT": + host, port = _split_host_port(target, 443) + with socket.create_connection((host, port), timeout=10) as server: + client.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n") + print("CONNECT 200 Connection Established", flush=True) + _sendall(server, body, throttle) + _relay_tunnel(client, server, throttle) + return + url = urlsplit(target) if not url.scheme or not url.hostname: host = _get_header(headers, "Host") @@ -205,7 +225,7 @@ def _forward_http(client, proxy_auth): ) with socket.create_connection((target_host, target_port), timeout=10) as server: - server.sendall(request) + _sendall(server, request, throttle) response, status_line = _read_http_response(server) client.sendall(response) @@ -282,7 +302,7 @@ def _forward_socks5(client): _relay_tunnel(client, server) -def _relay_tunnel(client, server): +def _relay_tunnel(client, server, throttle=0): readable_sockets = [client, server] request_method = None response_status = None @@ -304,7 +324,7 @@ def _relay_tunnel(client, server): except OSError: pass continue - target.sendall(data) + _sendall(target, data, throttle if source is client else 0) if logged: continue @@ -326,18 +346,18 @@ def _relay_tunnel(client, server): logged = True -def _handle_client(client, proxy_type, proxy_auth): +def _handle_client(client, proxy_type, proxy_auth, throttle): with client: try: if proxy_type == "socks5-proxy": _forward_socks5(client) else: - _forward_http(client, proxy_auth) + _forward_http(client, proxy_auth, throttle) except Exception as exc: print(f"proxy error: {exc}", file=sys.stderr, flush=True) -def serve(proxy_type, listen_host, port, proxy_auth, output): +def serve(proxy_type, listen_host, port, proxy_auth, output, throttle=0): family = socket.AF_INET6 if ":" in listen_host else socket.AF_INET with socket.socket(family, socket.SOCK_STREAM) as listener: listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -351,7 +371,7 @@ def serve(proxy_type, listen_host, port, proxy_auth, output): client, _ = listener.accept() thread = threading.Thread( target=_handle_client, - args=(client, proxy_type, proxy_auth), + args=(client, proxy_type, proxy_auth, throttle), daemon=True, ) thread.start() @@ -363,10 +383,22 @@ def main(): parser.add_argument("--listen-host", default="127.0.0.1") parser.add_argument("--port", type=int, default=0) parser.add_argument("--proxy-auth") + parser.add_argument( + "--throttle", type=int, default=0, help="Upload bytes per second" + ) parser.add_argument("--output", required=True) args = parser.parse_args() - - serve(args.type, args.listen_host, args.port, args.proxy_auth, args.output) + if args.throttle < 0: + parser.error("--throttle must be non-negative") + + serve( + args.type, + args.listen_host, + args.port, + args.proxy_auth, + args.output, + args.throttle, + ) if __name__ == "__main__": diff --git a/tests/test_e2e_sentry.py b/tests/test_e2e_sentry.py index 5925a8ca86..1284bcc9f5 100644 --- a/tests/test_e2e_sentry.py +++ b/tests/test_e2e_sentry.py @@ -23,7 +23,7 @@ import pytest import requests -from . import run, check_output +from . import run, check_output, Envelope from .conditions import ( has_breakpad, has_crashpad, @@ -31,6 +31,7 @@ has_native, is_asan, ) +from .proxy import start_proxy # Skip all tests if E2E env vars not configured pytestmark = [ @@ -212,6 +213,48 @@ def poll_sentry_for_event(test_id, max_attempts=POLL_MAX_ATTEMPTS): raise TimeoutError(error_msg) +def poll_sentry_for_attachments(event_id, name, max_attempts=20): + """Poll until the event lists the attachment called `name`.""" + org, project = get_sentry_org_project() + headers = get_sentry_headers() + api_base = get_sentry_api_base() + url = f"{api_base}/projects/{org}/{project}/events/{event_id}/attachments/" + last_error = None + + for attempt in range(max_attempts): + try: + response = requests.get(url, headers=headers, timeout=30) + last_error = f"Attachments API returned {response.status_code}: {response.text[:200]}" + print(f" Attempt {attempt + 1}/{max_attempts}: {last_error}") + if response.status_code == 200: + attachments = response.json() + if any(a.get("name") == name for a in attachments): + return attachments + except requests.RequestException as e: + last_error = str(e) + print(f" Request exception: {last_error}") + + if attempt + 1 < max_attempts: + time.sleep(POLL_INTERVAL) + + raise TimeoutError( + f"Attachment {name} for event {event_id} not found after {max_attempts} attempts. " + f"Last error: {last_error}" + ) + + +def assert_large_attachment(attachments): + large = [a for a in attachments if a.get("name") == ".sentry-large-attachment"] + assert len(large) == 1, f"Expected one large attachment, got {attachments}" + attachment = large[0] + assert ( + attachment.get("type") == "event.attachment" + ), f"Unexpected attachment type: {attachment}" + assert ( + attachment.get("size") == 104857600 + ), f"Unexpected attachment size: {attachment}" + + def get_exception_from_event(event): """ Extract exception data from Sentry API event response. @@ -863,3 +906,73 @@ def test_e2e_trace_propagation(cmake): assert downstream_trace["trace_id"] == upstream_trace["trace_id"] assert downstream_trace["parent_span_id"] == upstream_trace["span_id"] + + +@pytest.mark.skipif(not has_native, reason="native backend not available") +def test_e2e_tus(cmake): + """Verify a large crash attachment reaches Sentry after an interrupted upload.""" + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "native"}) + env = dict(os.environ, SENTRY_DSN=os.environ["SENTRY_E2E_DSN"]) + + # The daemon uploads the crash through a throttled proxy, so its shutdown + # timeout interrupts the upload and persists the envelope for the restart. + proxy, port = start_proxy("http-proxy", throttle=1024 * 1024) + try: + output = run_crash_e2e( + tmp_path, + "sentry_test_integration", + [ + "log", + "e2e-test", + "http-proxy", + "large-attachment", + "shutdown-timeout", + "10000", + "crash", + ], + env=dict(env, SENTRY_TEST_PROXY_PORT=str(port)), + wait_for_daemon=True, + ) + test_id = extract_test_id(output) + finally: + proxy.terminate() + proxy.communicate(timeout=10) + + envelopes = list(tmp_path.glob(".sentry-native/*.run/*.envelope")) + assert envelopes, "Interrupted upload should persist its envelope" + refs = [ + item + for envelope in envelopes + for item in Envelope.deserialize(envelope.read_bytes()) + if item.headers.get("content_type") + == "application/vnd.sentry.attachment-ref+json" + ] + assert len(refs) == 1, "Interrupted upload should retain its attachment" + location = refs[0].payload.json.get("location") + assert location, "Upload location should be persisted" + + output = check_output( + tmp_path, + "sentry_test_integration", + ["log", "trace", "no-setup", "shutdown-timeout", "120000"], + env=env, + stderr=subprocess.STDOUT, + ) + output = output.decode("utf-8", errors="replace") + + # The persisted upload must be resumed, not re-created: only HEAD targets + # the persisted location, and nothing may hit the creation URL. The curl + # client traces request lines, the winhttp client logs request URLs. + target = urlsplit(location) + assert ( + urlunsplit(("", "", target.path, target.query, "")) in output + ), "Resumed upload should query the offset of its location" + assert not re.search( + r'/upload/["\s]', output + ), "Resumed upload should not create a new upload" + + event = poll_sentry_for_event(test_id) + attachments = poll_sentry_for_attachments( + event["eventID"], ".sentry-large-attachment" + ) + assert_large_attachment(attachments) diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py index 97df4854be..f2b26c01bf 100644 --- a/tests/test_integration_tus.py +++ b/tests/test_integration_tus.py @@ -23,12 +23,45 @@ auth_header = ( f"Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/{SENTRY_VERSION}" ) -# sentry -upload_metadata = "sentry eyJhdHRhY2htZW50X3R5cGUiOiJldmVudC5taW5pZHVtcCJ9" +# sentry +upload_metadata = "sentry eyJhdHRhY2htZW50X3R5cGUiOiJldmVudC5hdHRhY2htZW50In0=" # fmt: on -def test_tus_upload(cmake, httpserver): +upload_size = 100 * 1024 * 1024 +chunk_size = 25 * 1024 * 1024 +chunk_headers = { + "Upload-Chunk-Granularity": "1048576", + "Upload-Min-Chunk-Size": "1048576", + "Upload-Max-Chunk-Size": str(chunk_size), +} + + +def expect_upload_chunks( + httpserver, upload_uri, upload_qs, final_location, offset=0, size=chunk_size +): + while offset < upload_size: + length = min(size, upload_size - offset) + headers = {"Upload-Offset": str(offset + length)} + if offset + length == upload_size: + headers["Location"] = final_location + httpserver.expect_oneshot_request( + upload_uri, + method="PATCH", + headers={ + "tus-resumable": "1.0.0", + "upload-offset": str(offset), + "content-length": str(length), + }, + query_string=upload_qs, + ).respond_with_data("", status=204, headers=headers) + offset += length + + +@pytest.mark.parametrize( + "granularity", [1, 6, None], ids=["chunked", "rounded", "single"] +) +def test_tus_upload(cmake, httpserver, granularity): tmp_path = cmake( ["sentry_test_integration"], {"SENTRY_BACKEND": "none"}, @@ -37,6 +70,18 @@ def test_tus_upload(cmake, httpserver): upload_uri = "/api/123456/upload/abc123def456789/" upload_qs = "length=104857600&signature=xyz" location = httpserver.url_for(upload_uri) + "?" + upload_qs + final_qs = "upload_length=104857600&upload_signature=xyz" + final_location = httpserver.url_for(upload_uri) + "?" + final_qs + + size = upload_size + headers = {"Location": location} + if granularity: + unit = granularity * 1024 * 1024 + size = chunk_size - chunk_size % unit + headers.update(chunk_headers) + headers["Upload-Chunk-Granularity"] = str(unit) + headers["Upload-Min-Chunk-Size"] = str(unit) + count = (upload_size + size - 1) // size # TUS creation request (POST, no body) -> 201 + Location httpserver.expect_oneshot_request( @@ -46,16 +91,16 @@ def test_tus_upload(cmake, httpserver): ).respond_with_data( "OK", status=201, - headers={"Location": location}, + headers=headers, ) - # TUS upload request (PATCH with body) -> 204 - httpserver.expect_oneshot_request( + expect_upload_chunks( + httpserver, upload_uri, - method="PATCH", - headers={"tus-resumable": "1.0.0"}, - query_string=upload_qs, - ).respond_with_data("", status=204) + upload_qs, + final_location, + size=size, + ) httpserver.expect_request( "/api/123456/envelope/", @@ -71,7 +116,7 @@ def test_tus_upload(cmake, httpserver): env=env, ) - assert len(httpserver.log) == 3 + assert len(httpserver.log) == count + 2 # Find the create, upload, and envelope requests create_req = None @@ -100,7 +145,7 @@ def test_tus_upload(cmake, httpserver): # Verify TUS upload request headers assert upload_req.headers.get("tus-resumable") == "1.0.0" assert upload_req.headers.get("content-type") == "application/offset+octet-stream" - assert upload_req.headers.get("upload-offset") == "0" + assert upload_req.headers.get("upload-offset") == str((count - 1) * size) # The envelope contains normal small attachments and the large # attachment-ref. @@ -118,11 +163,12 @@ def test_tus_upload(cmake, httpserver): attachment_ref = item break + # The placeholder uses the Location of the final PATCH response, not the + # one from the creation request. assert attachment_ref is not None - assert attachment_ref.payload.json["location"] == location - assert not os.path.isabs(attachment_ref.payload.json["path"]) + assert attachment_ref.payload.json["location"] == final_location assert attachment_ref.headers.get("attachment_length") == 100 * 1024 * 1024 - assert attachment_ref.headers.get("attachment_type") == "event.minidump" + assert attachment_ref.headers.get("attachment_type") == "event.attachment" # large attachment files should be cleaned up after send cache_dir = os.path.join(tmp_path, ".sentry-native", "cache") @@ -197,15 +243,10 @@ def test_tus_rate_limit(cmake, httpserver): ).respond_with_data( "OK", status=201, - headers={"Location": location}, + headers={"Location": location, **chunk_headers}, ) - httpserver.expect_oneshot_request( - upload_uri, - method="PATCH", - headers={"tus-resumable": "1.0.0"}, - query_string=upload_qs, - ).respond_with_data("", status=204) + expect_upload_chunks(httpserver, upload_uri, upload_qs, location) httpserver.expect_oneshot_request( "/api/123456/envelope/", @@ -248,9 +289,9 @@ def test_tus_rate_limit(cmake, httpserver): req for req, _resp in httpserver.log if req.path == "/api/123456/envelope/" ] - assert len(httpserver.log) == 3 + assert len(httpserver.log) == 6 assert len(uploads) == 1 - assert len(patches) == 1 + assert len(patches) == 4 assert len(envelopes) == 1 cache_dir = os.path.join(tmp_path, ".sentry-native", "cache") @@ -259,7 +300,8 @@ def test_tus_rate_limit(cmake, httpserver): assert leftovers == [] -def test_tus_shutdown(cmake, httpserver): +@pytest.mark.parametrize("chunked", [True, False], ids=["chunked", "legacy"]) +def test_tus_resume_after_shutdown(cmake, httpserver, chunked): tmp_path = cmake( ["sentry_test_integration"], {"SENTRY_BACKEND": "none"}, @@ -270,17 +312,31 @@ def test_tus_shutdown(cmake, httpserver): upload_uri = "/api/123456/upload/abc123def456789/" upload_qs = "length=104857600&signature=xyz" location = httpserver.url_for(upload_uri) + "?" + upload_qs - - release_create = threading.Event() - - def delayed_create(_req): - release_create.wait() - return Response("OK", status=201, headers={"Location": location}) + final_qs = "upload_length=104857600&upload_signature=xyz" + final_location = httpserver.url_for(upload_uri) + "?" + final_qs + upload_offset = chunk_size if chunked else upload_size // 2 httpserver.expect_oneshot_request( "/api/123456/upload/", headers={"tus-resumable": "1.0.0"}, - ).respond_with_handler(delayed_create) + ).respond_with_data( + "OK", + status=201, + headers={"Location": location, **(chunk_headers if chunked else {})}, + ) + + release_upload = threading.Event() + + def delayed_upload(_req): + release_upload.wait() + return Response("", status=204, headers={"Upload-Offset": str(upload_offset)}) + + httpserver.expect_oneshot_request( + upload_uri, + method="PATCH", + headers={"tus-resumable": "1.0.0", "upload-offset": "0"}, + query_string=upload_qs, + ).respond_with_handler(delayed_upload) with httpserver.wait(timeout=10): try: @@ -298,7 +354,7 @@ def delayed_create(_req): timeout=30, ) finally: - release_create.set() + release_upload.set() db_dir = os.path.join(tmp_path, ".sentry-native") cache_dir = os.path.join(db_dir, "cache") @@ -312,26 +368,176 @@ def delayed_create(_req): envelope_files = [f for f in os.listdir(run_dir) if f.endswith(".envelope")] assert len(envelope_files) == 1 base = envelope_files[0][: -len(".envelope")] + with open(os.path.join(run_dir, envelope_files[0]), "rb") as f: + persisted = Envelope.deserialize(f.read()) + ref = next(item for item in persisted if item.headers.get("type") == "attachment") + assert ref.payload.json.get("chunk_size", 0) == (chunk_size if chunked else 0) siblings = [f for f in os.listdir(cache_dir) if f.startswith(base)] assert len(siblings) > 0 + resume_marker = b"resumed-body" + with open(os.path.join(cache_dir, siblings[0]), "r+b") as attachment: + attachment.seek(upload_offset) + attachment.write(resume_marker) httpserver.clear_all_handlers() + httpserver.expect_oneshot_request( + upload_uri, + method="HEAD", + headers={"tus-resumable": "1.0.0"}, + query_string=upload_qs, + ).respond_with_data("", status=204, headers={"Upload-Offset": str(upload_offset)}) + + expect_upload_chunks( + httpserver, + upload_uri, + upload_qs, + final_location, + offset=upload_offset, + size=chunk_size if chunked else upload_size, + ) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + with httpserver.wait(timeout=10): + run( + tmp_path, + "sentry_test_integration", + [ + "log", + "no-setup", + # extra time for resuming 100 MiB upload + "shutdown-timeout", + "10000", + ], + env=env, + ) + + envelope_reqs = [ + entry[0] for entry in httpserver.log if "/envelope/" in entry[0].path + ] + assert len(envelope_reqs) == 1 + + envelope = Envelope.deserialize(envelope_reqs[0].get_data()) + attachment_ref = None + for item in envelope: + if ( + item.headers.get("content_type") + == "application/vnd.sentry.attachment-ref+json" + ): + if hasattr(item.payload, "json") and "location" in item.payload.json: + attachment_ref = item + break + + assert attachment_ref is not None + assert attachment_ref.payload.json["location"] == final_location + + upload_reqs = [ + req + for req, _resp in httpserver.log + if req.path == upload_uri and req.method == "PATCH" + ] + assert len(upload_reqs) == (4 if chunked else 2) + assert int(upload_reqs[1].headers["content-length"]) == ( + chunk_size if chunked else upload_size - upload_offset + ) + assert upload_reqs[1].get_data().startswith(resume_marker) + + leftover_siblings = [f for f in os.listdir(cache_dir) if f.startswith(base)] + assert leftover_siblings == [] + + +@pytest.mark.parametrize( + "status", [409, 503, 204], ids=["conflict", "unavailable", "partial"] +) +def test_tus_resume_after_retry(cmake, httpserver, status): + tmp_path = cmake( + ["sentry_test_integration"], + {"SENTRY_BACKEND": "none"}, + ) + + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + upload_uri = "/api/123456/upload/abc123def456789/" + upload_qs = "length=104857600&signature=xyz" + location = httpserver.url_for(upload_uri) + "?" + upload_qs + final_qs = "upload_length=104857600&upload_signature=xyz" + final_location = httpserver.url_for(upload_uri) + "?" + final_qs + upload_offset = 5 * 1024 * 1024 + httpserver.expect_oneshot_request( "/api/123456/upload/", headers={"tus-resumable": "1.0.0"}, ).respond_with_data( - "OK", - status=201, - headers={"Location": location}, + "OK", status=201, headers={"Location": location, **chunk_headers} ) + # The server only received part of the body, reported as an offset + # conflict, as unavailable, or as a partial Upload-Offset on the 204. httpserver.expect_oneshot_request( upload_uri, method="PATCH", + headers={"tus-resumable": "1.0.0", "upload-offset": "0"}, + query_string=upload_qs, + ).respond_with_data( + "", status=status, headers={"Upload-Offset": str(upload_offset)} + ) + + if status == 204: + expect_upload_chunks( + httpserver, upload_uri, upload_qs, final_location, offset=upload_offset + ) + httpserver.expect_oneshot_request( + "/api/123456/envelope/", headers={"x-sentry-auth": auth_header} + ).respond_with_data("OK") + + run( + tmp_path, + "sentry_test_integration", + ["log", "no-setup", "http-retry", "large-attachment", "capture-event"], + env=env, + ) + + if status == 204: + assert len(httpserver.log) == 7 + envelope_req = httpserver.log[-1][0] + envelope = Envelope.deserialize(envelope_req.get_data()) + ref = next( + item for item in envelope if item.headers.get("type") == "attachment" + ) + assert ref.payload.json["location"] == final_location + return + + # The envelope is kept for retry instead of being sent without the + # attachment-ref. + assert len(httpserver.log) == 2 + cache_dir = os.path.join(tmp_path, ".sentry-native", "cache") + envelope_files = [f for f in os.listdir(cache_dir) if f.endswith(".envelope")] + assert len(envelope_files) == 1 + assert "-00-" in envelope_files[0] + uuid = envelope_files[0][-len(".envelope") - 36 : -len(".envelope")] + siblings = [f for f in os.listdir(cache_dir) if f.startswith(uuid)] + assert len(siblings) > 0 + resume_marker = b"resumed-body" + with open(os.path.join(cache_dir, siblings[0]), "r+b") as attachment: + attachment.seek(upload_offset) + attachment.write(resume_marker) + + httpserver.clear_all_handlers() + + httpserver.expect_oneshot_request( + upload_uri, + method="HEAD", headers={"tus-resumable": "1.0.0"}, query_string=upload_qs, - ).respond_with_data("", status=204) + ).respond_with_data("", status=204, headers={"Upload-Offset": str(upload_offset)}) + + expect_upload_chunks( + httpserver, upload_uri, upload_qs, final_location, offset=upload_offset + ) httpserver.expect_oneshot_request( "/api/123456/envelope/", @@ -345,6 +551,7 @@ def delayed_create(_req): [ "log", "no-setup", + "http-retry", # extra time for resuming 100 MiB upload "shutdown-timeout", "10000", @@ -358,6 +565,7 @@ def delayed_create(_req): assert len(envelope_reqs) == 1 envelope = Envelope.deserialize(envelope_reqs[0].get_data()) + assert envelope.headers["event_id"] == uuid attachment_ref = None for item in envelope: if ( @@ -369,10 +577,20 @@ def delayed_create(_req): break assert attachment_ref is not None - assert attachment_ref.payload.json["location"] == location + assert attachment_ref.payload.json["location"] == final_location - leftover_siblings = [f for f in os.listdir(cache_dir) if f.startswith(base)] - assert leftover_siblings == [] + upload_reqs = [ + req + for req, _resp in httpserver.log + if req.path == upload_uri and req.method == "PATCH" + ] + assert len(upload_reqs) == 5 + assert int(upload_reqs[1].headers["content-length"]) == chunk_size + assert upload_reqs[1].get_data().startswith(resume_marker) + + leftovers = [f for f in os.listdir(cache_dir) if f.startswith(uuid)] + assert leftovers == [] + assert [f for f in os.listdir(cache_dir) if f.endswith(".envelope")] == [] @pytest.mark.parametrize( @@ -429,6 +647,8 @@ def test_tus_crash_restart(cmake, httpserver, backend): upload_uri = "/api/123456/upload/abc123def456789/" upload_qs = "length=104857600&signature=xyz" location = httpserver.url_for(upload_uri) + "?" + upload_qs + final_qs = "upload_length=104857600&upload_signature=xyz" + final_location = httpserver.url_for(upload_uri) + "?" + final_qs # Second run: restart picks up crash and uploads via TUS httpserver.expect_oneshot_request( @@ -437,15 +657,10 @@ def test_tus_crash_restart(cmake, httpserver, backend): ).respond_with_data( "OK", status=201, - headers={"Location": location}, + headers={"Location": location, **chunk_headers}, ) - httpserver.expect_oneshot_request( - upload_uri, - method="PATCH", - headers={"tus-resumable": "1.0.0"}, - query_string=upload_qs, - ).respond_with_data("", status=204) + expect_upload_chunks(httpserver, upload_uri, upload_qs, final_location) httpserver.expect_request( "/api/123456/envelope/", @@ -459,7 +674,7 @@ def test_tus_crash_restart(cmake, httpserver, backend): env=env, ) - assert len(httpserver.log) == 3 + assert len(httpserver.log) == 6 create_req = None upload_req = None @@ -485,7 +700,7 @@ def test_tus_crash_restart(cmake, httpserver, backend): # Verify TUS upload request headers assert upload_req.headers.get("tus-resumable") == "1.0.0" assert upload_req.headers.get("content-type") == "application/offset+octet-stream" - assert upload_req.headers.get("upload-offset") == "0" + assert upload_req.headers.get("upload-offset") == str(3 * chunk_size) # The envelope contains the event and the attachment-ref body = envelope_req.get_data() @@ -501,8 +716,7 @@ def test_tus_crash_restart(cmake, httpserver, backend): break assert attachment_ref is not None - assert attachment_ref.payload.json["location"] == location - assert not os.path.isabs(attachment_ref.payload.json["path"]) + assert attachment_ref.payload.json["location"] == final_location assert attachment_ref.headers.get("attachment_length") == 100 * 1024 * 1024 # Staged sibling files should be cleaned up after successful send. @@ -521,18 +735,17 @@ def test_tus_crash_native(cmake, httpserver): upload_uri = "/api/123456/upload/abc123def456789/" upload_qs = "length=104857600&signature=xyz" location = httpserver.url_for(upload_uri) + "?" + upload_qs + final_qs = "upload_length=104857600&upload_signature=xyz" + final_location = httpserver.url_for(upload_uri) + "?" + final_qs httpserver.expect_oneshot_request( "/api/123456/upload/", headers={"tus-resumable": "1.0.0"}, - ).respond_with_data("OK", status=201, headers={"Location": location}) + ).respond_with_data( + "OK", status=201, headers={"Location": location, **chunk_headers} + ) - httpserver.expect_oneshot_request( - upload_uri, - method="PATCH", - headers={"tus-resumable": "1.0.0"}, - query_string=upload_qs, - ).respond_with_data("", status=204) + expect_upload_chunks(httpserver, upload_uri, upload_qs, final_location) httpserver.expect_oneshot_request( "/api/123456/envelope/", @@ -584,7 +797,7 @@ def test_tus_crash_native(cmake, httpserver): attachment_ref = item break assert attachment_ref is not None - assert attachment_ref.payload.json["location"] == location + assert attachment_ref.payload.json["location"] == final_location db_dir = os.path.join(tmp_path, ".sentry-native") run_dirs = [ @@ -595,3 +808,208 @@ def test_tus_crash_native(cmake, httpserver): assert len(run_dirs) == 1 run_dir = os.path.join(db_dir, run_dirs[0]) assert not [f for f in os.listdir(run_dir) if f.endswith(".envelope")] + + +@pytest.mark.parametrize("status", [404, 410]) +@pytest.mark.parametrize("chunked", [True, False], ids=["chunked", "single"]) +def test_tus_recreate_after_head_404(cmake, httpserver, status, chunked): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) + cache_dir = tmp_path / ".sentry-native" / "cache" + cache_dir.mkdir(parents=True) + event_id = "b951e5e8-2b70-473f-91f3-667daf78d3de" + run_dir = tmp_path / ".sentry-native" / f"{event_id}.run" + run_dir.mkdir() + filename = f"{event_id}-attachment.bin" + with (cache_dir / filename).open("wb") as attachment: + attachment.truncate(upload_size) + + upload_uri = "/api/123456/upload/recreated/" + upload_qs = "length=104857600&signature=new" + location = httpserver.url_for(upload_uri) + "?" + upload_qs + final_location = httpserver.url_for(upload_uri) + "?upload_signature=final" + payload = { + "path": filename, + "location": "/api/123456/upload/expired/", + "content_type": "application/octet-stream", + "chunk_size": 32 * 1024 * 1024, + } + data = json.dumps(payload) + headers = { + "type": "attachment", + "content_type": "application/vnd.sentry.attachment-ref+json", + "filename": "attachment.bin", + "attachment_length": upload_size, + "length": len(data), + } + (run_dir / f"{event_id}.envelope").write_text( + json.dumps({"event_id": event_id.replace("-", "")}) + + "\n" + + json.dumps(headers) + + "\n" + + data + ) + + httpserver.expect_oneshot_request( + "/api/123456/upload/expired/", method="HEAD" + ).respond_with_data("", status=status) + httpserver.expect_oneshot_request( + "/api/123456/upload/", method="POST" + ).respond_with_data( + "", + status=201, + headers={"Location": location, **(chunk_headers if chunked else {})}, + ) + expect_upload_chunks( + httpserver, + upload_uri, + upload_qs, + final_location, + size=chunk_size if chunked else upload_size, + ) + httpserver.expect_oneshot_request( + "/api/123456/envelope/", headers={"x-sentry-auth": auth_header} + ).respond_with_data("OK") + + run( + tmp_path, + "sentry_test_integration", + ["log", "no-setup", "shutdown-timeout", "10000"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + assert len(httpserver.log) == (7 if chunked else 4) + envelope = Envelope.deserialize(httpserver.log[-1][0].get_data()) + ref = next(iter(envelope)) + assert ref.payload.json["location"] == final_location + assert not list(cache_dir.iterdir()) + + +@pytest.mark.parametrize( + "offset", + [None, "0", "-1", "invalid", "26214400", "104857601", "18446744073709551616"], +) +def test_tus_invalid_offset(cmake, httpserver, offset): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) + upload_uri = "/api/123456/upload/invalid-offset/" + httpserver.expect_oneshot_request( + "/api/123456/upload/", method="POST" + ).respond_with_data( + "", status=201, headers={"Location": upload_uri, **chunk_headers} + ) + httpserver.expect_oneshot_request( + upload_uri, method="PATCH", headers={"upload-offset": "0"} + ).respond_with_data( + "", + status=204, + headers={"Upload-Offset": str(chunk_size), "Location": "/non-final/"}, + ) + httpserver.expect_oneshot_request( + upload_uri, method="PATCH", headers={"upload-offset": str(chunk_size)} + ).respond_with_data( + "", status=204, headers={"Upload-Offset": offset} if offset is not None else {} + ) + + run( + tmp_path, + "sentry_test_integration", + ["log", "no-setup", "http-retry", "large-attachment", "capture-event"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + assert len(httpserver.log) == 3 + cache_dir = tmp_path / ".sentry-native" / "cache" + envelopes = list(cache_dir.glob("*.envelope")) + assert len(envelopes) == 1 + envelope = Envelope.deserialize(envelopes[0].read_bytes()) + ref = next(item for item in envelope if item.headers.get("type") == "attachment") + assert ref.payload.json["location"] == upload_uri + assert ref.payload.json["chunk_size"] == chunk_size + + +@pytest.mark.parametrize( + "has_location", [True, False], ids=["location", "missing-location"] +) +def test_tus_resume_completed_upload(cmake, httpserver, has_location): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "none"}) + cache_dir = tmp_path / ".sentry-native" / "cache" + cache_dir.mkdir(parents=True) + event_id = "b951e5e8-2b70-473f-91f3-667daf78d3de" + run_dir = tmp_path / ".sentry-native" / f"{event_id}.run" + run_dir.mkdir() + filename = f"{event_id}-attachment.bin" + with (cache_dir / filename).open("wb") as attachment: + attachment.truncate(upload_size) + + upload_uri = "/api/123456/upload/completed/" + upload_qs = "length=104857600&signature=xyz" + location = upload_uri + "?" + upload_qs + final_qs = "upload_length=104857600&upload_signature=xyz" + final_location = httpserver.url_for(upload_uri) + "?" + final_qs + payload = { + "path": filename, + "location": location, + "content_type": "application/octet-stream", + "chunk_size": chunk_size, + } + data = json.dumps(payload) + headers = { + "type": "attachment", + "content_type": "application/vnd.sentry.attachment-ref+json", + "filename": "attachment.bin", + "attachment_length": upload_size, + "length": len(data), + } + (run_dir / f"{event_id}.envelope").write_text( + json.dumps({"event_id": event_id.replace("-", "")}) + + "\n" + + json.dumps(headers) + + "\n" + + data + ) + + httpserver.expect_oneshot_request( + upload_uri, method="HEAD", query_string=upload_qs + ).respond_with_data("", status=204, headers={"Upload-Offset": str(upload_size)}) + httpserver.expect_oneshot_request( + upload_uri, + method="PATCH", + headers={ + "tus-resumable": "1.0.0", + "content-type": "application/offset+octet-stream", + "upload-offset": str(upload_size), + }, + query_string=upload_qs, + data=b"", + ).respond_with_data( + "", + status=204, + headers={ + "Upload-Offset": str(upload_size), + **({"Location": final_location} if has_location else {}), + }, + ) + httpserver.expect_oneshot_request( + "/api/123456/envelope/", headers={"x-sentry-auth": auth_header} + ).respond_with_data("OK") + + run( + tmp_path, + "sentry_test_integration", + ["log", "no-setup", "http-retry"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + assert len(httpserver.log) == (3 if has_location else 2) + if has_location: + envelope = Envelope.deserialize(httpserver.log[-1][0].get_data()) + ref = next(iter(envelope)) + assert ref.payload.json["location"] == final_location + assert not list(cache_dir.iterdir()) + else: + envelopes = list(cache_dir.glob("*.envelope")) + assert len(envelopes) == 1 + envelope = Envelope.deserialize(envelopes[0].read_bytes()) + ref = next(iter(envelope)) + assert ref.payload.json["location"] == location + assert ref.payload.json["chunk_size"] == chunk_size + assert (cache_dir / filename).is_file() diff --git a/tests/test_proxy_server.py b/tests/test_proxy_server.py index ab3cb0b164..1088b339a2 100644 --- a/tests/test_proxy_server.py +++ b/tests/test_proxy_server.py @@ -57,7 +57,7 @@ def start_proxy(tmp_path): processes = [] proxy_server = Path(__file__).with_name("proxy_server.py") - def start(proxy_type, proxy_auth=None): + def start(proxy_type, proxy_auth=None, throttle=0): output = tmp_path / "proxy-{}.port".format(len(processes)) command = [ sys.executable, @@ -74,6 +74,8 @@ def start(proxy_type, proxy_auth=None): ] if proxy_auth: command += ["--proxy-auth", proxy_auth] + if throttle: + command += ["--throttle", str(throttle)] process = subprocess.Popen( command, @@ -218,3 +220,46 @@ def test_socks5_tunneling(start_proxy, target_server): assert response.endswith(b"OK") method, path, _, body = target_server.requests.get(timeout=5) assert (method, path, body) == ("GET", "/through-socks", b"") + + +@pytest.mark.parametrize("tunnel", [False, True], ids=["http", "connect"]) +def test_upload_throttle(start_proxy, target_server, tunnel): + throttle = 128 * 1024 + port = start_proxy("http-proxy", throttle=throttle) + target_port = target_server.server_address[1] + body = b"x" * (64 * 1024) + + with socket.create_connection(("127.0.0.1", port), timeout=5) as client: + if tunnel: + client.sendall( + ( + f"CONNECT 127.0.0.1:{target_port} HTTP/1.1\r\n" + f"Host: 127.0.0.1:{target_port}\r\n\r\n" + ).encode("ascii") + ) + response = b"HTTP/1.1 200 Connection Established\r\n\r\n" + assert _recv_exact(client, len(response)) == response + + target = "/upload" if tunnel else f"http://127.0.0.1:{target_port}/upload" + request = ( + f"POST {target} HTTP/1.1\r\n" + f"Host: 127.0.0.1:{target_port}\r\n" + f"Content-Length: {len(body)}\r\n" + "Connection: close\r\n\r\n" + ).encode("ascii") + body + started = time.monotonic() + client.sendall(request) + client.shutdown(socket.SHUT_WR) + response = b"" + while True: + chunk = client.recv(64 * 1024) + if not chunk: + break + response += chunk + elapsed = time.monotonic() - started + + assert response.startswith(b"HTTP/1.1 200") + assert response.endswith(b"OK") + method, path, _, received = target_server.requests.get(timeout=5) + assert (method, path, received) == ("POST", "/upload", body) + assert elapsed >= len(body) / throttle diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index c8683c7434..7a0d78099c 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -1500,3 +1500,36 @@ SENTRY_TEST(minidump_rate_limit) sentry__rate_limiter_free(rl); sentry_envelope_free(envelope); } + +SENTRY_TEST(attachment_ref_chunk_size_roundtrip) +{ + size_t sizes[] = { 32 * 1024 * 1024, SIZE_MAX }; + for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) { + sentry_envelope_t *envelope = sentry__envelope_new(NULL); + sentry_attachment_ref_t ref = { 0 }; + ref.path = "abc-crashlog.bin"; + sentry_envelope_item_t *item = sentry__envelope_add_attachment_ref( + envelope, &ref, "crashlog.bin", NULL, 12345); + TEST_ASSERT(!!item); + TEST_CHECK(sentry__envelope_item_get_attachment_ref(item, &ref)); + TEST_CHECK_INT_EQUAL(ref.chunk_size, 0); + sentry__attachment_ref_cleanup(&ref); + TEST_CHECK(sentry__envelope_item_resolve_attachment_ref( + item, "abc-crashlog.bin", "/upload/abc", sizes[i])); + + size_t len = 0; + char *buf = sentry_envelope_serialize(envelope, &len); + TEST_ASSERT(!!buf); + sentry_envelope_free(envelope); + envelope = sentry_envelope_deserialize(buf, len); + sentry_free(buf); + TEST_ASSERT(!!envelope); + item = sentry__envelope_get_item(envelope, 0); + TEST_CHECK(sentry__envelope_item_get_attachment_ref(item, &ref)); + TEST_CHECK_STRING_EQUAL(ref.path, "abc-crashlog.bin"); + TEST_CHECK_STRING_EQUAL(ref.location, "/upload/abc"); + TEST_CHECK(ref.chunk_size == sizes[i]); + sentry__attachment_ref_cleanup(&ref); + sentry_envelope_free(envelope); + } +} diff --git a/tests/unit/test_http_transport.c b/tests/unit/test_http_transport.c index b3a3552b32..b3a63f8690 100644 --- a/tests/unit/test_http_transport.c +++ b/tests/unit/test_http_transport.c @@ -18,14 +18,27 @@ SENTRY_TEST(http_response_set_header_sets_known_headers) sentry_http_response_set_header( &resp, "x-sentry-rate-limits", "60:error:key"); sentry_http_response_set_header(&resp, "location", "/uploads/1"); + sentry_http_response_set_header(&resp, "upload-offset", "42"); + sentry_http_response_set_header(&resp, "upload-chunk-granularity", "256"); + sentry_http_response_set_header(&resp, "upload-min-chunk-size", "1048576"); + sentry_http_response_set_header( + &resp, "upload-max-chunk-size", "1073741824"); TEST_CHECK_STRING_EQUAL(resp.retry_after, "60"); TEST_CHECK_STRING_EQUAL(resp.x_sentry_rate_limits, "60:error:key"); TEST_CHECK_STRING_EQUAL(resp.location, "/uploads/1"); + TEST_CHECK_STRING_EQUAL(resp.upload_offset, "42"); + TEST_CHECK_STRING_EQUAL(resp.upload_chunk_granularity, "256"); + TEST_CHECK_STRING_EQUAL(resp.upload_min_chunk_size, "1048576"); + TEST_CHECK_STRING_EQUAL(resp.upload_max_chunk_size, "1073741824"); sentry_free(resp.retry_after); sentry_free(resp.x_sentry_rate_limits); sentry_free(resp.location); + sentry_free(resp.upload_offset); + sentry_free(resp.upload_chunk_granularity); + sentry_free(resp.upload_min_chunk_size); + sentry_free(resp.upload_max_chunk_size); } SENTRY_TEST(http_response_set_header_is_case_insensitive) @@ -37,14 +50,27 @@ SENTRY_TEST(http_response_set_header_is_case_insensitive) sentry_http_response_set_header( &resp, "X-SENTRY-RATE-LIMITS", "30:error:key"); sentry_http_response_set_header(&resp, "Location", "/uploads/2"); + sentry_http_response_set_header(&resp, "Upload-Offset", "84"); + sentry_http_response_set_header(&resp, "Upload-Chunk-Granularity", "256"); + sentry_http_response_set_header(&resp, "Upload-Min-Chunk-Size", "1048576"); + sentry_http_response_set_header( + &resp, "Upload-Max-Chunk-Size", "1073741824"); TEST_CHECK_STRING_EQUAL(resp.retry_after, "30"); TEST_CHECK_STRING_EQUAL(resp.x_sentry_rate_limits, "30:error:key"); TEST_CHECK_STRING_EQUAL(resp.location, "/uploads/2"); + TEST_CHECK_STRING_EQUAL(resp.upload_offset, "84"); + TEST_CHECK_STRING_EQUAL(resp.upload_chunk_granularity, "256"); + TEST_CHECK_STRING_EQUAL(resp.upload_min_chunk_size, "1048576"); + TEST_CHECK_STRING_EQUAL(resp.upload_max_chunk_size, "1073741824"); sentry_free(resp.retry_after); sentry_free(resp.x_sentry_rate_limits); sentry_free(resp.location); + sentry_free(resp.upload_offset); + sentry_free(resp.upload_chunk_granularity); + sentry_free(resp.upload_min_chunk_size); + sentry_free(resp.upload_max_chunk_size); } SENTRY_TEST(http_response_set_header_ignores_unknown_headers) @@ -58,6 +84,10 @@ SENTRY_TEST(http_response_set_header_ignores_unknown_headers) TEST_CHECK(resp.retry_after == NULL); TEST_CHECK(resp.x_sentry_rate_limits == NULL); TEST_CHECK(resp.location == NULL); + TEST_CHECK(resp.upload_offset == NULL); + TEST_CHECK(resp.upload_chunk_granularity == NULL); + TEST_CHECK(resp.upload_min_chunk_size == NULL); + TEST_CHECK(resp.upload_max_chunk_size == NULL); } SENTRY_TEST(http_response_set_header_overwrites_previous_value) @@ -165,6 +195,7 @@ SENTRY_TEST(http_request_accessors_file_backed_body) req->headers_len = 0; req->body_path = sentry__path_from_str("/tmp/does-not-need-to-exist"); req->body_len = 100 * 1024 * 1024; + req->body_offset = 42; size_t len = 0; TEST_CHECK(sentry_http_request_get_body(req, &len) == NULL); @@ -174,6 +205,8 @@ SENTRY_TEST(http_request_accessors_file_backed_body) const char *path = sentry_http_request_get_body_file_path(req, &len); TEST_CHECK_STRING_EQUAL(path, "/tmp/does-not-need-to-exist"); TEST_CHECK_INT_EQUAL((int)len, 100 * 1024 * 1024); + TEST_CHECK_INT_EQUAL( + (int)sentry_http_request_get_body_file_offset(req), 42); #ifdef SENTRY_PLATFORM_WINDOWS // The wide variant must stay in sync with the narrow one, since that is @@ -210,6 +243,7 @@ SENTRY_TEST(http_request_accessors_bodyless_request) len = 123; TEST_CHECK(sentry_http_request_get_body_file_path(req, &len) == NULL); TEST_CHECK_INT_EQUAL((int)len, 0); + TEST_CHECK_INT_EQUAL((int)sentry_http_request_get_body_file_offset(req), 0); sentry__prepared_http_request_free(req); } @@ -223,6 +257,8 @@ SENTRY_TEST(http_request_accessors_null_safety) size_t len = 0; TEST_CHECK(sentry_http_request_get_body(NULL, &len) == NULL); TEST_CHECK(sentry_http_request_get_body_file_path(NULL, &len) == NULL); + TEST_CHECK_INT_EQUAL( + (int)sentry_http_request_get_body_file_offset(NULL), 0); } static void * diff --git a/tests/unit/test_tus.c b/tests/unit/test_tus.c index 7009bb92b0..c9d0879858 100644 --- a/tests/unit/test_tus.c +++ b/tests/unit/test_tus.c @@ -65,12 +65,14 @@ SENTRY_TEST(tus_request_preparation) } if (strcmp(req->headers[i].key, "upload-metadata") == 0) { has_upload_metadata = true; + TEST_CHECK_STRING_EQUAL(req->headers[i].value, + "sentry eyJhdHRhY2htZW50X3R5cGUiOiJldmVudC5hdHRhY2htZW50In0="); } } TEST_CHECK(has_tus_resumable); TEST_CHECK(has_upload_length); TEST_CHECK(!has_content_type); - TEST_CHECK(!has_upload_metadata); + TEST_CHECK(has_upload_metadata); sentry__prepared_http_request_free(req); @@ -96,7 +98,7 @@ SENTRY_TEST(tus_request_preparation) const char *location = "https://sentry.invalid/api/42/upload/abc123/"; req = sentry__prepare_tus_upload_request( - location, test_file_path, 9, dsn, NULL); + location, test_file_path, 9, 0, 9, dsn, NULL); TEST_CHECK(!!req); TEST_CHECK_STRING_EQUAL(req->method, "PATCH"); TEST_CHECK_STRING_EQUAL(req->url, location); @@ -127,6 +129,29 @@ SENTRY_TEST(tus_request_preparation) TEST_CHECK(has_upload_offset); sentry__prepared_http_request_free(req); + + // Test a resumed upload starts at the requested file offset. + req = sentry__prepare_tus_upload_request( + location, test_file_path, 9, 4, 3, dsn, NULL); + TEST_CHECK(!!req); + TEST_CHECK_INT_EQUAL(req->body_offset, 4); + TEST_CHECK_INT_EQUAL(req->body_len, 3); + has_upload_offset = false; + for (size_t i = 0; i < req->headers_len; i++) { + if (strcmp(req->headers[i].key, "upload-offset") == 0) { + TEST_CHECK_STRING_EQUAL(req->headers[i].value, "4"); + has_upload_offset = true; + } + } + TEST_CHECK(has_upload_offset); + sentry__prepared_http_request_free(req); + + TEST_CHECK(!sentry__prepare_tus_upload_request( + location, test_file_path, 9, 10, 0, dsn, NULL)); + + TEST_CHECK(!sentry__prepare_tus_upload_request( + location, test_file_path, 9, 4, 6, dsn, NULL)); + sentry__path_remove(test_file_path); sentry__path_free(test_file_path); sentry__dsn_decref(dsn); @@ -251,8 +276,9 @@ SENTRY_TEST(tus_upload_error) } // Mock that drives a full TUS round-trip: returns a relative `Location` from -// the create POST (per spec), 204 from the upload PATCH, and captures the -// final envelope POST body so the test can inspect the placeholder payload. +// the create POST (per spec), 204 with the final `Location` from the upload +// PATCH, and captures the final envelope POST body so the test can inspect +// the placeholder payload. typedef struct { char *captured_body; size_t captured_body_len; @@ -262,6 +288,8 @@ typedef struct { static const char *TUS_RELATIVE_LOCATION = "/api/42/upload/019db3e0/?length=104857600&signature=test"; +static const char *TUS_FINAL_LOCATION + = "/api/42/upload/019db3e0/?upload_length=104857600&upload_signature=test"; static int tus_capture_send(void *client, sentry_prepared_http_request_t *req, @@ -280,6 +308,8 @@ tus_capture_send(void *client, sentry_prepared_http_request_t *req, if (strcmp(req->method, "PATCH") == 0 && req->body_path) { cap->patch_count++; resp->status_code = 204; + resp->location = sentry__string_clone(TUS_FINAL_LOCATION); + resp->upload_offset = sentry__string_clone("104857600"); return true; } // Envelope POST: capture body @@ -341,7 +371,9 @@ SENTRY_TEST(tus_placeholder_uses_raw_location) if (!sentry__envelope_item_get_attachment_ref(item, &ref)) { continue; } - TEST_CHECK_STRING_EQUAL(ref.location, TUS_RELATIVE_LOCATION); + // The placeholder must carry the final PATCH `Location`, not the + // one from the create POST. + TEST_CHECK_STRING_EQUAL(ref.location, TUS_FINAL_LOCATION); sentry__attachment_ref_cleanup(&ref); found_ref = true; } @@ -455,3 +487,105 @@ SENTRY_TEST(tus_attachment_ref_confinement) sentry__path_free(db); #endif } + +SENTRY_TEST(tus_chunk_size) +{ + struct { + const char *granularity; + const char *min; + const char *max; + bool valid; + size_t size; + } cases[] = { + { "256", "1048576", "1073741824", true, 32 * 1024 * 1024 }, + { "1048576", "1048576", "26214400", true, 25 * 1024 * 1024 }, + { "1048576", "41943040", "1073741824", true, 40 * 1024 * 1024 }, + { "5242880", "5242880", "1073741824", true, 30 * 1024 * 1024 }, + { "10485760", "32505856", "104857600", true, 40 * 1024 * 1024 }, + { "67108864", "1048576", "134217728", true, 64 * 1024 * 1024 }, + { NULL, "1048576", "1073741824", true, 0 }, + { "256", NULL, "1073741824", true, 0 }, + { "256", "1048576", NULL, true, 0 }, + { "0", "1048576", "1073741824", false, 0 }, + { "256", "0", "1073741824", false, 0 }, + { "256", "1048576", "0", false, 0 }, + { "256", "1048576", "1024", false, 0 }, + { "1024", "513", "1023", false, 0 }, + { "", "1048576", "1073741824", false, 0 }, + { "-1", "1048576", "1073741824", false, 0 }, + { "256", "+1048576", "1073741824", false, 0 }, + { "256", "1048576", "1073741824x", false, 0 }, + { "256", "1048576", "18446744073709551616", false, 0 }, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + sentry_http_response_t resp = { 0 }; + resp.upload_chunk_granularity = (char *)cases[i].granularity; + resp.upload_min_chunk_size = (char *)cases[i].min; + resp.upload_max_chunk_size = (char *)cases[i].max; + size_t size = 0; + TEST_CHECK( + sentry__tus_choose_chunk_size(&resp, &size) == cases[i].valid); + TEST_CHECK_INT_EQUAL(size, cases[i].size); + } +} + +static int +tus_rate_limit_send(void *client, sentry_prepared_http_request_t *req, + sentry_http_response_t *resp) +{ + int *create_count = client; + if (strcmp(req->method, "POST") == 0 && !req->body && !req->body_path) { + (*create_count)++; + resp->status_code = 429; + resp->retry_after = sentry__string_clone("60"); + return true; + } + resp->status_code = 200; + return true; +} + +SENTRY_TEST(tus_rate_limit) +{ +#if defined(SENTRY_PLATFORM_ANDROID) || defined(SENTRY_PLATFORM_NX) \ + || defined(SENTRY_PLATFORM_PS) || defined(SENTRY_PLATFORM_XBOX) + SKIP_TEST(); +#else + const char *test_file_str + = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_rate_limit"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + create_large_test_file(test_file_str); + + int create_count = 0; + sentry_transport_t *transport + = sentry__http_transport_new(&create_count, tus_rate_limit_send); + TEST_ASSERT(!!transport); + sentry__client_report_reset(); + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_transport(options, transport); + sentry_options_set_enable_large_attachments(options, 1); + sentry_options_add_attachment(options, test_file_str); + sentry_init(options); + + sentry_capture_event( + sentry_value_new_message_event(SENTRY_LEVEL_INFO, NULL, "test")); + + sentry_close(); + + // The upload's 429 rate-limits the event without a send error report. + TEST_CHECK_INT_EQUAL(create_count, 1); + sentry_client_report_t report = { { 0 } }; + TEST_CHECK(sentry__client_report_save(&report)); + TEST_CHECK_INT_EQUAL(report.counts[SENTRY_DISCARD_REASON_SEND_ERROR] + [SENTRY_DATA_CATEGORY_ATTACHMENT], + 0); + TEST_CHECK_INT_EQUAL(report.counts[SENTRY_DISCARD_REASON_RATELIMIT_BACKOFF] + [SENTRY_DATA_CATEGORY_ERROR], + 1); + sentry__client_report_reset(); + + sentry__path_remove(test_file_path); + sentry__path_free(test_file_path); +#endif +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 3dbb8d3e31..5ebb85fbbc 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -19,6 +19,7 @@ XX(attachment_properties) XX(attachment_rate_limit) XX(attachment_ref_cache_cleanup) XX(attachment_ref_cache_discard) +XX(attachment_ref_chunk_size_roundtrip) XX(attachment_ref_copy) XX(attachment_ref_creation) XX(attachment_ref_from_path) @@ -564,8 +565,10 @@ XX(transport_sampling_transactions) XX(transport_sampling_transactions_set_trace) XX(transport_suspend) XX(tus_attachment_ref_confinement) +XX(tus_chunk_size) XX(tus_file_attachment_preserves_original) XX(tus_placeholder_uses_raw_location) +XX(tus_rate_limit) XX(tus_request_preparation) XX(tus_upload_error) XX(tus_upload_url) From 9c97815cacddad7f3b6f53a2c77266319fec449e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Nov 2026 14:41:55 +0100 Subject: [PATCH 46/47] feat(tus): gzip upload chunks with transport compression (#2222) With `SENTRY_TRANSPORT_COMPRESSION`, PATCH chunks up to the default chunk size are read into memory and sent gzip-compressed, with `content-encoding: gzip` and `x-decoded-content-length`. Larger bodies, such as unchunked uploads, are still streamed from the file. The curl client now sets the request method for in-memory bodies, so a buffered chunk is sent as PATCH instead of POST. --- .github/workflows/e2e-test.yml | 7 ++- CHANGELOG.md | 1 + src/transports/sentry_http_transport.c | 51 +++++++++++++++++++-- src/transports/sentry_http_transport_curl.c | 1 + tests/test_e2e_sentry.py | 39 ++++++++++++++++ tests/test_integration_tus.py | 50 +++++++++++++++++--- tests/unit/test_tus.c | 8 +++- 7 files changed, 143 insertions(+), 14 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index ff5a67ce10..3a16ad4a5a 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -35,7 +35,12 @@ jobs: if: runner.os == 'Linux' run: | sudo apt update - sudo apt install -y cmake libcurl4-openssl-dev + sudo apt install -y cmake libcurl4-openssl-dev zlib1g-dev + + - name: Set up zlib for Windows + if: runner.os == 'Windows' + shell: powershell + run: . "scripts\install-zlib.ps1" - name: Set up Python uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a04062eeb..a5d393fc85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Add `sentry_continue_trace` to continue incoming traces without a transaction. ([#2237](https://github.com/getsentry/sentry-native/pull/2237)) - Add `sentry_iter_trace_propagation_headers` to propagate traces without a span. ([#2240](https://github.com/getsentry/sentry-native/pull/2240)) - Resume interrupted TUS attachment uploads from the server-provided upload offset. ([#2058](https://github.com/getsentry/sentry-native/pull/2058)) +- Compress TUS attachment uploads when `SENTRY_TRANSPORT_COMPRESSION` is enabled. ([#2222](https://github.com/getsentry/sentry-native/pull/2222)) **Fixes**: diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 61aa358828..c5c9f60bd0 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -22,11 +22,12 @@ #define ENVELOPE_MIME "application/x-sentry-envelope" #define TUS_MIME "application/offset+octet-stream" #define TUS_CHUNK_SIZE (32 * 1024 * 1024) // 32 MiB -#define TUS_MAX_HTTP_HEADERS 4 #ifdef SENTRY_TRANSPORT_COMPRESSION # define MAX_HTTP_HEADERS 4 +# define TUS_MAX_HTTP_HEADERS 6 #else # define MAX_HTTP_HEADERS 3 +# define TUS_MAX_HTTP_HEADERS 4 #endif typedef struct { @@ -97,6 +98,23 @@ gzipped_with_compression(const char *body, const size_t body_len, deflateEnd(&stream); return true; } + +static bool +gzipped_file_chunk(const sentry_path_t *path, size_t offset, size_t len, + char **compressed_body, size_t *compressed_body_len) +{ + FILE *file = sentry__path_open(path, "rb", offset); + if (!file) { + return false; + } + char *chunk = sentry_malloc(len); + bool compressed = chunk && fread(chunk, 1, len, file) == len + && gzipped_with_compression( + chunk, len, compressed_body, compressed_body_len); + sentry_free(chunk); + fclose(file); + return compressed; +} #endif sentry_prepared_http_request_t * @@ -307,9 +325,22 @@ prepare_tus_upload_request(const char *location, const sentry_path_t *path, req->method = "PATCH"; req->url = sentry__string_clone(location); - req->body_path = sentry__path_clone(path); - req->body_len = body_len; - req->body_offset = upload_offset; + + bool compressed = false; +#ifdef SENTRY_TRANSPORT_COMPRESSION + // Compression buffers the chunk in memory, so skip oversized chunks such + // as unchunked uploads. + if (body_len <= TUS_CHUNK_SIZE) { + compressed = gzipped_file_chunk( + path, upload_offset, body_len, &req->body, &req->body_len); + req->body_owned = compressed; + } +#endif + if (!compressed) { + req->body_path = sentry__path_clone(path); + req->body_len = body_len; + req->body_offset = upload_offset; + } sentry_prepared_http_header_t *h; h = &req->headers[req->headers_len++]; @@ -328,6 +359,18 @@ prepare_tus_upload_request(const char *location, const sentry_path_t *path, h->key = "upload-offset"; h->value = sentry__uint64_to_string((uint64_t)upload_offset); +#ifdef SENTRY_TRANSPORT_COMPRESSION + if (compressed) { + h = &req->headers[req->headers_len++]; + h->key = "content-encoding"; + h->value = sentry__string_clone("gzip"); + + h = &req->headers[req->headers_len++]; + h->key = "x-decoded-content-length"; + h->value = sentry__uint64_to_string((uint64_t)body_len); + } +#endif + return req; } diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 26f920cd69..67dba8d842 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -488,6 +488,7 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)req->body_len); } else if (req->body) { g_curl.easy_setopt(curl, CURLOPT_POST, (long)1); + g_curl.easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method); g_curl.easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); g_curl.easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); } else if (sentry__string_eq(req->method, "HEAD")) { diff --git a/tests/test_e2e_sentry.py b/tests/test_e2e_sentry.py index 1284bcc9f5..35d97fceb6 100644 --- a/tests/test_e2e_sentry.py +++ b/tests/test_e2e_sentry.py @@ -976,3 +976,42 @@ def test_e2e_tus(cmake): event["eventID"], ".sentry-large-attachment" ) assert_large_attachment(attachments) + + +def test_e2e_tus_compressed(cmake): + """Verify a large attachment reaches Sentry in gzip-compressed chunks.""" + tmp_path = cmake( + ["sentry_test_integration"], + {"SENTRY_BACKEND": "none", "SENTRY_TRANSPORT_COMPRESSION": "On"}, + ) + env = dict(os.environ, SENTRY_DSN=os.environ["SENTRY_E2E_DSN"]) + + output = check_output( + tmp_path, + "sentry_test_integration", + [ + "log", + "trace", + "e2e-test", + "large-attachment", + "capture-event", + "shutdown-timeout", + "120000", + ], + env=env, + stderr=subprocess.STDOUT, + ) + test_id = extract_test_id(output) + output = output.decode("utf-8", errors="replace") + + # Envelopes are compressed too, but only upload chunks carry the decoded + # length. + assert ( + "x-decoded-content-length:" in output + ), "Upload chunks should be gzip-compressed" + + event = poll_sentry_for_event(test_id) + attachments = poll_sentry_for_attachments( + event["eventID"], ".sentry-large-attachment" + ) + assert_large_attachment(attachments) diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py index f2b26c01bf..0fe3d8d399 100644 --- a/tests/test_integration_tus.py +++ b/tests/test_integration_tus.py @@ -11,7 +11,11 @@ Envelope, SENTRY_VERSION, ) -from .assertions import assert_attachment +from .assertions import ( + assert_attachment, + assert_gzip_content_encoding, + assert_gzip_file_header, +) from .conditions import has_breakpad, has_files, has_http, has_native, is_qemu pytestmark = [ @@ -38,20 +42,31 @@ def expect_upload_chunks( - httpserver, upload_uri, upload_qs, final_location, offset=0, size=chunk_size + httpserver, + upload_uri, + upload_qs, + final_location, + offset=0, + size=chunk_size, + compressed=False, ): while offset < upload_size: length = min(size, upload_size - offset) headers = {"Upload-Offset": str(offset + length)} if offset + length == upload_size: headers["Location"] = final_location + length_header = ( + {"x-decoded-content-length": str(length)} + if compressed + else {"content-length": str(length)} + ) httpserver.expect_oneshot_request( upload_uri, method="PATCH", headers={ "tus-resumable": "1.0.0", "upload-offset": str(offset), - "content-length": str(length), + **length_header, }, query_string=upload_qs, ).respond_with_data("", status=204, headers=headers) @@ -59,13 +74,21 @@ def expect_upload_chunks( @pytest.mark.parametrize( - "granularity", [1, 6, None], ids=["chunked", "rounded", "single"] + "granularity, build_args", + [ + (1, {}), + (6, {}), + (None, {}), + (1, {"SENTRY_TRANSPORT_COMPRESSION": "On"}), + ], + ids=["chunked", "rounded", "single", "compressed"], ) -def test_tus_upload(cmake, httpserver, granularity): +def test_tus_upload(cmake, httpserver, granularity, build_args): tmp_path = cmake( ["sentry_test_integration"], - {"SENTRY_BACKEND": "none"}, + {"SENTRY_BACKEND": "none", **build_args}, ) + compressed = build_args.get("SENTRY_TRANSPORT_COMPRESSION") == "On" upload_uri = "/api/123456/upload/abc123def456789/" upload_qs = "length=104857600&signature=xyz" @@ -100,6 +123,7 @@ def test_tus_upload(cmake, httpserver, granularity): upload_qs, final_location, size=size, + compressed=compressed, ) httpserver.expect_request( @@ -112,7 +136,16 @@ def test_tus_upload(cmake, httpserver, granularity): run( tmp_path, "sentry_test_integration", - ["log", "no-setup", "attachment", "large-attachment", "capture-event"], + [ + "log", + "no-setup", + "attachment", + "large-attachment", + "capture-event", + # extra time for compressing 100 MiB + "shutdown-timeout", + "30000", + ], env=env, ) @@ -146,6 +179,9 @@ def test_tus_upload(cmake, httpserver, granularity): assert upload_req.headers.get("tus-resumable") == "1.0.0" assert upload_req.headers.get("content-type") == "application/offset+octet-stream" assert upload_req.headers.get("upload-offset") == str((count - 1) * size) + if compressed: + assert_gzip_content_encoding(upload_req) + assert_gzip_file_header(upload_req.get_data()) # The envelope contains normal small attachments and the large # attachment-ref. diff --git a/tests/unit/test_tus.c b/tests/unit/test_tus.c index c9d0879858..37be6fb80b 100644 --- a/tests/unit/test_tus.c +++ b/tests/unit/test_tus.c @@ -102,9 +102,11 @@ SENTRY_TEST(tus_request_preparation) TEST_CHECK(!!req); TEST_CHECK_STRING_EQUAL(req->method, "PATCH"); TEST_CHECK_STRING_EQUAL(req->url, location); +#ifndef SENTRY_TRANSPORT_COMPRESSION TEST_CHECK(!!req->body_path); TEST_CHECK_INT_EQUAL(req->body_len, 9); TEST_CHECK(!req->body); +#endif has_tus_resumable = false; has_content_type = false; @@ -134,8 +136,10 @@ SENTRY_TEST(tus_request_preparation) req = sentry__prepare_tus_upload_request( location, test_file_path, 9, 4, 3, dsn, NULL); TEST_CHECK(!!req); +#ifndef SENTRY_TRANSPORT_COMPRESSION TEST_CHECK_INT_EQUAL(req->body_offset, 4); TEST_CHECK_INT_EQUAL(req->body_len, 3); +#endif has_upload_offset = false; for (size_t i = 0; i < req->headers_len; i++) { if (strcmp(req->headers[i].key, "upload-offset") == 0) { @@ -304,8 +308,8 @@ tus_capture_send(void *client, sentry_prepared_http_request_t *req, resp->location = sentry__string_clone(TUS_RELATIVE_LOCATION); return true; } - // TUS upload: PATCH with body_path - if (strcmp(req->method, "PATCH") == 0 && req->body_path) { + // TUS upload: PATCH + if (strcmp(req->method, "PATCH") == 0) { cap->patch_count++; resp->status_code = 204; resp->location = sentry__string_clone(TUS_FINAL_LOCATION); From ad4bbc582fd901efaaa13e1acb5c874d49de5691 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Nov 2026 14:56:56 +0100 Subject: [PATCH 47/47] fix(curl): Stop file uploads at the request body length The curl read callback streamed the request body file until EOF. Chunked TUS uploads start at `body_offset` and send `body_len` bytes, but `CURLOPT_INFILESIZE_LARGE` does not limit the body. libcurl before 8.7.0 keeps reading past the chunk when the chunk size is not a multiple of its upload buffer, sending more data than the declared `Content-Length`. Cap reads at `body_len` as the WinHTTP transport does. --- src/transports/sentry_http_transport_curl.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 67dba8d842..fcef16302f 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -202,6 +202,7 @@ typedef struct { typedef struct { FILE *file; const sentry_path_t *path; + size_t remaining; } file_body_t; static curl_client_t * @@ -411,10 +412,14 @@ file_read_callback(char *buffer, size_t size, size_t nitems, void *userdata) goto fail; } size_t capacity = size * nitems; + if (capacity > body->remaining) { + capacity = body->remaining; + } size_t read = fread(buffer, 1, capacity, body->file); if (read < capacity && ferror(body->file)) { goto fail; } + body->remaining -= read; return read; fail: @@ -480,6 +485,7 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, } file_body.file = body_file; file_body.path = req->body_path; + file_body.remaining = req->body_len; g_curl.easy_setopt(curl, CURLOPT_UPLOAD, 1L); g_curl.easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method); g_curl.easy_setopt(curl, CURLOPT_READFUNCTION, file_read_callback);