From dde4b66125b1a33b126859a92ce9bc6df3436171 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 12 Oct 2026 10:21:33 +0200 Subject: [PATCH 1/4] fix(crashpad): Preserve the Windows handler across backend reinstall `sentry_reinstall_backend()` currently destroys the Crashpad client and starts another handler with a new IPC pipe. Startup also replays scope breadcrumbs without resetting the ring counter, duplicating entries. Add a Windows reinstall hook that calls `CrashpadClient::RegisterHandlers()` on the existing client, preserving the handler process, pipe, and backend state. Reinstall fails if startup left no client; Linux and macOS retain their restart path. This is a prerequisite for the handler-side attachment buffering planned for #2182. Breadcrumb and scope updates will remain in the Windows handler until a report is built, so switching handlers during reinstall would strand pending data. Linux and macOS write attachments directly. Ref: #2182 --- external/crashpad | 2 +- src/backends/sentry_backend_crashpad.cpp | 17 +++++++++++++++++ tests/test_integration_crashpad.py | 8 +++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/external/crashpad b/external/crashpad index d0fb04940..36eaa9756 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit d0fb04940eeaf22dda8bfe0ae894d0fe96bdfcdf +Subproject commit 36eaa9756449dac38632b76cc08771fec3ce2600 diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 5e49e9ccc..b2f5cda66 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -1407,6 +1407,20 @@ crashpad_backend_prune_database(sentry_backend_t *backend) crashpad::PruneCrashReportDatabase(data->db, &condition); } +#ifdef SENTRY_PLATFORM_WINDOWS +static int +crashpad_backend_reinstall(sentry_backend_t *backend) +{ + auto *data = static_cast(backend->data); + if (!data->client) { + return 1; + } + + data->client->RegisterHandlers(); + return 0; +} +#endif + void sentry__backend_preload(void) { @@ -1437,6 +1451,9 @@ sentry__backend_new(void) backend->get_last_crash_func = crashpad_backend_last_crash; backend->process_old_run_func = crashpad_backend_process_old_run; backend->prune_database_func = crashpad_backend_prune_database; +#ifdef SENTRY_PLATFORM_WINDOWS + backend->reinstall_func = crashpad_backend_reinstall; +#endif backend->data = data; backend->can_capture_after_shutdown = true; diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index f2214f644..39687bb40 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -376,6 +376,9 @@ def test_crashpad_crash_proxy(cmake, httpserver, run_args, proxy_running): def test_crashpad_reinstall(cmake, httpserver): tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "crashpad"}) + args = ["log"] + if sys.platform == "win32": + args.append("overflow-breadcrumbs") env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK") @@ -384,7 +387,7 @@ def test_crashpad_reinstall(cmake, httpserver): run( tmp_path, "sentry_test_integration", - ["log", "reinstall", "crash"], + [*args, "reinstall", "crash"], expect_failure=True, env=env, ) @@ -395,6 +398,9 @@ def test_crashpad_reinstall(cmake, httpserver): assert len(httpserver.log) == 1 + if sys.platform == "win32": + assert_crashpad_upload(httpserver.log[0][0]) + @pytest.mark.skipif( sys.platform == "darwin", From 5ba7afa0103c12de40acd6bad5f240024f78452d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 12 Oct 2026 10:33:42 +0200 Subject: [PATCH 2/4] fix(crashpad): Avoid Windows handler disk waits for scope updates Update Crashpad to buffer breadcrumb and scope attachment updates in the handler and acknowledge each update after it is stored. Report creation flushes that data before reading attachments or the external envelope. This removes disk I/O from pipe service threads and preserves per-file order for the SDK's serialized writers. Acknowledgment remains synchronous so accepted updates survive a client crash, including crashes handled through WER. The preceding reinstall change keeps the handler and its pending data when another runtime replaces the crash handlers. Linux and macOS write attachments directly. Overflow assertions now require the exact retained sequence and account for the fixture's three initial breadcrumbs. A shortened payload cannot fall back to the initial-breadcrumb check. Ref: #2182 --- CHANGELOG.md | 1 + external/crashpad | 2 +- tests/assertions.py | 35 ++++++++------- tests/fixtures/integration.c | 10 +++-- tests/test_integration_crashpad.py | 69 +++++++++++++++++++++++++++++- 5 files changed, 96 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00063c18e..159c7680e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - The persisted installation ID is no longer required to be a UUID, so a platform integration can supply any ID that `user.id` accepts. ([#2177](https://github.com/getsentry/sentry-native/pull/2177)) - Parsing a timestamp with a malformed fractional-second part no longer writes a leftover debug line to `stdout`. ([#2187](https://github.com/getsentry/sentry-native/pull/2187)) +- Crashpad/Windows: Breadcrumb and scope IPC updates are no longer blocked by earlier disk writes keeping the handler's pipe instances busy, and consecutive updates are applied in order. ([#2182](https://github.com/getsentry/sentry-native/issues/2182)) **Other changes**: diff --git a/external/crashpad b/external/crashpad index 36eaa9756..e4f03174a 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 36eaa9756449dac38632b76cc08771fec3ce2600 +Subproject commit e4f03174a959bc9980ec69e3b2b7fc18c5ab775b diff --git a/tests/assertions.py b/tests/assertions.py index ca86d58d1..9176a63e2 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -585,21 +585,22 @@ def is_valid_timestamp(timestamp): return False -def _validate_breadcrumb_seq(seq, breadcrumb_func): - for i in seq: - breadcrumb = breadcrumb_func(i) - assert breadcrumb["message"] == str(i) - assert is_valid_timestamp(breadcrumb["timestamp"]) - - -def assert_overflowing_breadcrumb(attachments): - if len(attachments.breadcrumb1) > 3: - _validate_breadcrumb_seq(range(97), lambda i: attachments.breadcrumb1[3 + i]) - _validate_breadcrumb_seq( - range(97, 101), lambda i: attachments.breadcrumb2[i - 97] +def assert_overflowing_breadcrumb(attachments, count): + max_breadcrumbs = 100 + messages = ["default level is info", "debug crumb", "lf\ncrlf\r\nlf\n..."] + messages += [str(i) for i in range(count)] + total = len(messages) + start = max(0, ((total - 1) // max_breadcrumbs - 1) * max_breadcrumbs) + for file, breadcrumbs in enumerate( + (attachments.breadcrumb1, attachments.breadcrumb2) + ): + expected = [ + messages[i] for i in range(start, total) if i // max_breadcrumbs % 2 == file + ] + assert [breadcrumb["message"] for breadcrumb in breadcrumbs] == expected + assert all( + is_valid_timestamp(breadcrumb["timestamp"]) for breadcrumb in breadcrumbs ) - else: - assert_breadcrumb_inner(attachments.breadcrumb1) def assert_crashpad_upload( @@ -608,13 +609,17 @@ def assert_crashpad_upload( expect_view_hierarchy=False, expect_breadcrumbs=True, expect_default_scope=True, + breadcrumb_count=0, ): multipart = gzip.decompress(req.get_data()) msg = email.message_from_bytes(bytes(str(req.headers), encoding="utf8") + multipart) attachments = _load_crashpad_attachments(msg) if expect_breadcrumbs: - assert_overflowing_breadcrumb(attachments) + if breadcrumb_count: + assert_overflowing_breadcrumb(attachments, breadcrumb_count) + else: + assert_breadcrumb_inner(attachments.breadcrumb1) if expect_default_scope: assert_event_meta(attachments.event, integrations=["crashpad"]) if expect_attachment: diff --git a/tests/fixtures/integration.c b/tests/fixtures/integration.c index b70f2e9a0..87b6bf8db 100644 --- a/tests/fixtures/integration.c +++ b/tests/fixtures/integration.c @@ -1223,9 +1223,13 @@ main(int argc, char **argv) } if (has_arg(argc, argv, "overflow-breadcrumbs")) { - for (size_t i = 0; i < 101; i++) { - char buffer[4]; - snprintf(buffer, 4, "%zu", i); + const char *breadcrumb_count + = get_arg_value(argc, argv, "breadcrumb-count"); + size_t count + = breadcrumb_count ? strtoull(breadcrumb_count, NULL, 10) : 101; + for (size_t i = 0; i < count; i++) { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%zu", i); sentry_add_breadcrumb(sentry_value_new_breadcrumb(0, buffer)); } } diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index 39687bb40..e3b488606 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -399,7 +399,7 @@ def test_crashpad_reinstall(cmake, httpserver): assert len(httpserver.log) == 1 if sys.platform == "win32": - assert_crashpad_upload(httpserver.log[0][0]) + assert_crashpad_upload(httpserver.log[0][0], breadcrumb_count=101) @pytest.mark.skipif( @@ -523,7 +523,10 @@ def test_crashpad_wer_crash(cmake, httpserver, run_args): assert_session(envelope, {"status": "crashed", "errors": 1}) assert_crashpad_upload( - multipart, expect_attachment=True, expect_view_hierarchy=True + multipart, + expect_attachment=True, + expect_view_hierarchy=True, + breadcrumb_count=101, ) assert wait_for_no_werfault() @@ -614,6 +617,7 @@ def test_crashpad_dumping_crash(cmake, httpserver, run_args, build_args): multipart, expect_attachment=expect_attachments, expect_view_hierarchy=expect_attachments, + breadcrumb_count=101, ) callback = next( (arg for arg in run_args if arg in ("before-send", "on-crash")), None @@ -1419,3 +1423,64 @@ def test_crashpad_early_init(cmake): [], ) assert result.returncode == 0 + + +@pytest.mark.parametrize("count", [97, 98, 101, 997, 1000, 1097]) +@pytest.mark.parametrize( + "reinstall", + [ + False, + pytest.param( + True, + marks=pytest.mark.skipif( + sys.platform != "win32", + reason="reinstall keeps the handler only on Windows", + ), + ), + ], +) +@pytest.mark.parametrize( + "crash_arg", + [ + "crash", + pytest.param( + "fastfail", + marks=[ + pytest.mark.with_wer, + pytest.mark.skipif( + sys.platform != "win32" or bool(os.environ.get("TEST_MINGW")), + reason="fast-fail is only available in MSVC Windows builds", + ), + ], + ), + ], +) +def test_crashpad_breadcrumbs(cmake, httpserver, count, reinstall, crash_arg): + tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "crashpad"}) + args = [ + "log", + "overflow-breadcrumbs", + "breadcrumb-count", + str(count), + "crashpad-wait-for-upload", + ] + if reinstall: + args.append("reinstall") + + httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_test_integration", + [*args, crash_arg], + expect_failure=True, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + assert waiting.result + assert len(httpserver.log) == 1 + assert_crashpad_upload(httpserver.log[0][0], breadcrumb_count=count) + + if crash_arg == "fastfail": + assert wait_for_no_werfault() From 7c95ac8894837c650a30651be0d8c8c63b88fb22 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 12 Oct 2026 11:48:03 +0200 Subject: [PATCH 3/4] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 159c7680e..0b4448c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - The persisted installation ID is no longer required to be a UUID, so a platform integration can supply any ID that `user.id` accepts. ([#2177](https://github.com/getsentry/sentry-native/pull/2177)) - Parsing a timestamp with a malformed fractional-second part no longer writes a leftover debug line to `stdout`. ([#2187](https://github.com/getsentry/sentry-native/pull/2187)) -- Crashpad/Windows: Breadcrumb and scope IPC updates are no longer blocked by earlier disk writes keeping the handler's pipe instances busy, and consecutive updates are applied in order. ([#2182](https://github.com/getsentry/sentry-native/issues/2182)) +- Crashpad/Windows: Breadcrumb and scope IPC updates are no longer blocked by earlier disk writes keeping the handler's pipe instances busy. ([#2190](https://github.com/getsentry/sentry-native/pull/2190), [crashpad#174](https://github.com/getsentry/crashpad/pull/174)) **Other changes**: From c51c19c0a84ae2d38949918b45bf59c82e913b5a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 12 Oct 2026 13:17:12 +0200 Subject: [PATCH 4/4] Bump crashpad --- external/crashpad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/crashpad b/external/crashpad index e4f03174a..493151b22 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit e4f03174a959bc9980ec69e3b2b7fc18c5ab775b +Subproject commit 493151b224a6536bcedc3467d042863a063addc4