Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Bugsnag.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,14 @@
E1F167812F655D76002B14C0 /* BSGLoadConfigTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E1F1677F2F655D76002B14C0 /* BSGLoadConfigTests.m */; };
E1F167822F655D76002B14C0 /* BSGLoadConfigTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E1F1677F2F655D76002B14C0 /* BSGLoadConfigTests.m */; };
E1F167832F655D76002B14C0 /* BSGLoadConfigTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E1F1677F2F655D76002B14C0 /* BSGLoadConfigTests.m */; };
E1F3943130406F4D00D512BA /* KSCrashSentry_CPPException_Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E1F3943030406F1400D512BA /* KSCrashSentry_CPPException_Tests.mm */; };
E1F3943230406F4D00D512BA /* KSCrashSentry_CPPException_Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E1F3943030406F1400D512BA /* KSCrashSentry_CPPException_Tests.mm */; };
E1F3943330406F4D00D512BA /* KSCrashSentry_CPPException_Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E1F3943030406F1400D512BA /* KSCrashSentry_CPPException_Tests.mm */; };
E1F3943430406F4D00D512BA /* KSCrashSentry_CPPException_Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E1F3943030406F1400D512BA /* KSCrashSentry_CPPException_Tests.mm */; };
E1F394363040707B00D512BA /* BSG_KSCrashSentry_CPPException_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E1F394353040707700D512BA /* BSG_KSCrashSentry_CPPException_Private.h */; };
E1F394373040707B00D512BA /* BSG_KSCrashSentry_CPPException_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E1F394353040707700D512BA /* BSG_KSCrashSentry_CPPException_Private.h */; };
E1F394383040707B00D512BA /* BSG_KSCrashSentry_CPPException_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E1F394353040707700D512BA /* BSG_KSCrashSentry_CPPException_Private.h */; };
E1F394393040707B00D512BA /* BSG_KSCrashSentry_CPPException_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E1F394353040707700D512BA /* BSG_KSCrashSentry_CPPException_Private.h */; };
E701FA9F2490EF4A008D842F /* BugsnagApiValidationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E701FA9E2490EF4A008D842F /* BugsnagApiValidationTest.m */; };
E701FAA02490EF4A008D842F /* BugsnagApiValidationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E701FA9E2490EF4A008D842F /* BugsnagApiValidationTest.m */; };
E701FAA12490EF4A008D842F /* BugsnagApiValidationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E701FA9E2490EF4A008D842F /* BugsnagApiValidationTest.m */; };
Expand Down Expand Up @@ -2051,6 +2059,8 @@
CBEC893E2A4ACD230088A3CE /* FileBasedTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FileBasedTest.h; sourceTree = "<group>"; };
CBEC893F2A4ACD230088A3CE /* FileBasedTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileBasedTest.m; sourceTree = "<group>"; };
E1F1677F2F655D76002B14C0 /* BSGLoadConfigTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGLoadConfigTests.m; sourceTree = "<group>"; };
E1F3943030406F1400D512BA /* KSCrashSentry_CPPException_Tests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = KSCrashSentry_CPPException_Tests.mm; sourceTree = "<group>"; };
E1F394353040707700D512BA /* BSG_KSCrashSentry_CPPException_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSG_KSCrashSentry_CPPException_Private.h; sourceTree = "<group>"; };
E701FA9E2490EF4A008D842F /* BugsnagApiValidationTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugsnagApiValidationTest.m; sourceTree = "<group>"; };
E701FAA62490EF77008D842F /* ClientApiValidationTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ClientApiValidationTest.m; sourceTree = "<group>"; };
E701FAAA2490EFD9008D842F /* EventApiValidationTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EventApiValidationTest.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2169,6 +2179,7 @@
008966D32486D43700DC48C2 /* KSCrashTests */ = {
isa = PBXGroup;
children = (
E1F3943030406F1400D512BA /* KSCrashSentry_CPPException_Tests.mm */,
01ABD8782786DF9A009A5CA2 /* BSG_KSCrashReportTests.m */,
01B74E9B27903326004B9765 /* BSG_KSFileTests.m */,
008966D82486D43700DC48C2 /* BSG_KSMachHeadersTests.m */,
Expand Down Expand Up @@ -2315,6 +2326,7 @@
0089693A2486DAD000DC48C2 /* Sentry */ = {
isa = PBXGroup;
children = (
E1F394353040707700D512BA /* BSG_KSCrashSentry_CPPException_Private.h */,
008969402486DAD000DC48C2 /* BSG_KSCrashSentry_CPPException.h */,
008969422486DAD000DC48C2 /* BSG_KSCrashSentry_CPPException.mm */,
008969432486DAD000DC48C2 /* BSG_KSCrashSentry_MachException.c */,
Expand Down Expand Up @@ -2976,6 +2988,7 @@
96A143812E7D76EF009138C3 /* BSGEventDiscardRule.h in Headers */,
3A700AA224A63ADC0068CD1B /* BugsnagPlugin.h in Headers */,
3A700AA324A63ADC0068CD1B /* BugsnagDevice.h in Headers */,
E1F394393040707B00D512BA /* BSG_KSCrashSentry_CPPException_Private.h in Headers */,
3A700AA424A63ADC0068CD1B /* BugsnagApp.h in Headers */,
3A700AA524A63ADC0068CD1B /* BugsnagError.h in Headers */,
969EE10D2E7A9DC200600F63 /* BSG_KSCxaThrowSwapper.h in Headers */,
Expand Down Expand Up @@ -3103,6 +3116,7 @@
96828AFD2F0A9F1F0020821F /* BSGPathNode.h in Headers */,
96828B012F0A9F1F0020821F /* BSGIndexPathNode.h in Headers */,
96A143982E7D8746009138C3 /* BSGAllEventsDiscardRule.h in Headers */,
E1F394363040707B00D512BA /* BSG_KSCrashSentry_CPPException_Private.h in Headers */,
3A700AAE24A63CFD0068CD1B /* BSG_KSCrashReportWriter.h in Headers */,
3A700AAF24A63CFD0068CD1B /* BugsnagErrorTypes.h in Headers */,
969EE1092E7A9DC200600F63 /* BSG_KSCxaThrowSwapper.h in Headers */,
Expand Down Expand Up @@ -3245,6 +3259,7 @@
96828AAF2F0A9F1F0020821F /* BSGPathNode.h in Headers */,
96828AB32F0A9F1F0020821F /* BSGIndexPathNode.h in Headers */,
96A143992E7D8746009138C3 /* BSGAllEventsDiscardRule.h in Headers */,
E1F394383040707B00D512BA /* BSG_KSCrashSentry_CPPException_Private.h in Headers */,
3A700AC224A63D110068CD1B /* BSG_KSCrashReportWriter.h in Headers */,
3A700AC324A63D110068CD1B /* BugsnagErrorTypes.h in Headers */,
969EE1082E7A9DC200600F63 /* BSG_KSCxaThrowSwapper.h in Headers */,
Expand Down Expand Up @@ -3379,6 +3394,7 @@
96828B522F0AB6620020821F /* BSGJsonDataExtractor.h in Headers */,
96828B532F0AB6620020821F /* BSGJsonDataExtractorFactory.h in Headers */,
96828B542F0AB6620020821F /* BSGSimplePathExtractor.h in Headers */,
E1F394373040707B00D512BA /* BSG_KSCrashSentry_CPPException_Private.h in Headers */,
CBBDE919280068560070DCD3 /* BugsnagSystemState.h in Headers */,
CBBDE98D2800698F0070DCD3 /* BSG_KSCrashReportFields.h in Headers */,
01FF490928BF8B7B001F817B /* BugsnagInternals.h in Headers */,
Expand Down Expand Up @@ -4015,6 +4031,7 @@
008967692486D43700DC48C2 /* BugsnagSessionTrackerTest.m in Sources */,
9627A13B2D92201B00696E3C /* WriterTestsSupport.m in Sources */,
CB3744A428475F2A00A3955E /* BSG_KSCrashStringConversionTest.m in Sources */,
E1F3943330406F4D00D512BA /* KSCrashSentry_CPPException_Tests.mm in Sources */,
008967032486D43700DC48C2 /* BugsnagThreadSerializationTest.m in Sources */,
0089674B2486D43700DC48C2 /* BSGConnectivityTest.m in Sources */,
008967002486D43700DC48C2 /* BugsnagEventPersistLoadTest.m in Sources */,
Expand Down Expand Up @@ -4218,6 +4235,7 @@
019480D42625F3EB00E833ED /* BSGAppKitTests.m in Sources */,
008967462486D43700DC48C2 /* BugsnagTests.m in Sources */,
004E353D2487B3B8007FBAE4 /* BugsnagSwiftTests.swift in Sources */,
E1F3943230406F4D00D512BA /* KSCrashSentry_CPPException_Tests.mm in Sources */,
008967192486D43700DC48C2 /* BugsnagErrorTest.m in Sources */,
008967162486D43700DC48C2 /* BugsnagCollectionsTests.m in Sources */,
008967582486D43700DC48C2 /* BugsnagClientMirrorTest.m in Sources */,
Expand Down Expand Up @@ -4501,6 +4519,7 @@
0130DEFB2880203A00E5953F /* BSGRunContextTests.m in Sources */,
008967412486D43700DC48C2 /* BugsnagAppTest.m in Sources */,
017DCF9D287422BB000ECB22 /* BSGTelemetryTests.m in Sources */,
E1F3943130406F4D00D512BA /* KSCrashSentry_CPPException_Tests.mm in Sources */,
008967052486D43700DC48C2 /* BugsnagThreadSerializationTest.m in Sources */,
E1F167832F655D76002B14C0 /* BSGLoadConfigTests.m in Sources */,
008966FF2486D43700DC48C2 /* BugsnagOnBreadcrumbTest.m in Sources */,
Expand Down Expand Up @@ -4838,6 +4857,7 @@
CB28F0AD28294D4F003AB200 /* KSJSONCodec_Tests.m in Sources */,
CB28F0A528294D4F003AB200 /* RFC3339DateTool_Tests.m in Sources */,
CB28F0E2282A4BEF003AB200 /* BugsnagStackframeTest.m in Sources */,
E1F3943430406F4D00D512BA /* KSCrashSentry_CPPException_Tests.mm in Sources */,
CB28F0CF282A4A2E003AB200 /* BugsnagClientTests.m in Sources */,
CB28F0E0282A4BEE003AB200 /* BugsnagOnBreadcrumbTest.m in Sources */,
96828B152F0AA3C90020821F /* BSGJsonCollectionPathTests.m in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
//#define BSG_KSLogger_LocalLevel TRACE
#include "BSG_KSLogger.h"

#include <atomic>
#include <cxxabi.h>
#include <dlfcn.h>
#include <exception>
Expand All @@ -61,19 +62,20 @@
#pragma mark - Globals -
// ============================================================================

/** True if this handler has been installed. */
static volatile sig_atomic_t bsg_g_installed = 0;
/** Process-wide capture gate, also read by throwing threads. */
static std::atomic<bool> bsg_g_installed{false};
static_assert(ATOMIC_BOOL_LOCK_FREE == 2, "Crash handling requires lock-free atomics");
Comment thread
SB-meiyalaganr marked this conversation as resolved.

/** True if the handler should capture the next stack trace. */
static bool bsg_g_captureNextStackTrace = false;
/** True if this thread should capture throws, except while inspecting an exception. */
static thread_local bool bsg_g_captureNextStackTrace = true;

static std::terminate_handler bsg_g_originalTerminateHandler;

/** Buffer for the backtrace of the most recent exception. */
static uintptr_t bsg_g_stackTrace[STACKTRACE_BUFFER_LENGTH];
/** Backtrace of the most recent exception on this thread. */
static thread_local uintptr_t bsg_g_stackTrace[STACKTRACE_BUFFER_LENGTH];

/** Number of backtrace entries in the most recent exception. */
static int bsg_g_stackTraceCount = 0;
/** Number of backtrace entries in this thread's most recent exception. */
static thread_local int bsg_g_stackTraceCount = 0;

/** Context to fill with crash information. */
static BSG_KSCrash_SentryContext *bsg_g_context;
Expand All @@ -86,11 +88,11 @@

extern "C" {
void BSG__cxa_throw_override(void *thrown_exception, std::type_info *tinfo,
void (*dest)(void *)) __attribute__((weak));
void (*dest)(void *)) __attribute__((weak, noinline));

void BSG__cxa_throw_override(void *thrown_exception, std::type_info *tinfo,
void (*dest)(void *)) {
if (bsg_g_captureNextStackTrace) {
if (bsg_g_installed.load(std::memory_order_relaxed) && bsg_g_captureNextStackTrace) {
bsg_g_stackTraceCount =
backtrace((void **)bsg_g_stackTrace,
sizeof(bsg_g_stackTrace) / sizeof(*bsg_g_stackTrace));
Expand Down Expand Up @@ -125,18 +127,14 @@ static void CPPExceptionTerminate(void) {
char descriptionBuff[DESCRIPTION_BUFFER_LENGTH];
const char *name = NULL;
const char *crashReason = NULL;
// BSG__cxa_throw_override and __cxa_throw_decorator.
int framesToSkip = 2;

BSG_KSLOG_DEBUG("Get exception type name.");
std::type_info *tinfo = __cxxabiv1::__cxa_current_exception_type();
if (tinfo == NULL) {
name = "std::terminate";
crashReason = "throw may have been called without an exception";
if (!bsg_g_stackTraceCount) {
BSG_KSLOG_DEBUG("No exception backtrace");
bsg_g_stackTraceCount =
backtrace((void **)bsg_g_stackTrace,
sizeof(bsg_g_stackTrace) / sizeof(*bsg_g_stackTrace));
}
goto after_rethrow; // Using goto to avoid indenting code below
}

Expand Down Expand Up @@ -206,7 +204,16 @@ static void CPPExceptionTerminate(void) {
}

after_rethrow:
bsg_g_captureNextStackTrace = (bsg_g_installed != 0);
bsg_g_captureNextStackTrace = true;

// A bare terminate must not reuse a previously caught exception's trace.
// Also provide a fallback when the current exception's throw was not captured.
if (tinfo == NULL || bsg_g_stackTraceCount <= 0) {
bsg_g_stackTraceCount =
backtrace((void **)bsg_g_stackTrace,
sizeof(bsg_g_stackTrace) / sizeof(*bsg_g_stackTrace));
framesToSkip = 1; // CPPExceptionTerminate only, not the throw wrappers.
}

if (bsg_kscrashsentry_beginHandlingCrash(bsg_ksmachthread_self())) {

Expand All @@ -224,9 +231,11 @@ static void CPPExceptionTerminate(void) {
bsg_g_context->crashType = BSG_KSCrashTypeCPPException;
bsg_g_context->requiresAsyncSafety = true;
bsg_g_context->registersAreValid = false;
bsg_g_context->stackTrace =
bsg_g_stackTrace + 1; // Don't record __cxa_throw stack entry
bsg_g_context->stackTraceLength = bsg_g_stackTraceCount - 1;
const int stackTraceLength = bsg_g_stackTraceCount > framesToSkip
? bsg_g_stackTraceCount - framesToSkip : 0;
bsg_g_context->stackTrace = stackTraceLength > 0
? bsg_g_stackTrace + framesToSkip : NULL;
bsg_g_context->stackTraceLength = stackTraceLength;
bsg_g_context->CPPException.name = name;
bsg_g_context->crashReason = crashReason;

Expand Down Expand Up @@ -254,26 +263,24 @@ static void CPPExceptionTerminate(void) {
BSG_KSCrash_SentryContext *context) {
BSG_KSLOG_DEBUG("Installing C++ exception handler.");

if (bsg_g_installed) {
if (bsg_g_installed.load(std::memory_order_relaxed)) {
return true;
}
bsg_g_installed = 1;
bsg_g_installed.store(true, std::memory_order_relaxed);

bsg_g_context = context;

bsg_g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);
bsg_g_captureNextStackTrace = true;
bsg_ksct_swap(BSG__cxa_throw_override);
return true;
}

extern "C" void bsg_kscrashsentry_uninstallCPPExceptionHandler(void) {
BSG_KSLOG_DEBUG("Uninstalling C++ exception handler.");
if (!bsg_g_installed) {
if (!bsg_g_installed.load(std::memory_order_relaxed)) {
return;
}

bsg_g_captureNextStackTrace = false;
std::set_terminate(bsg_g_originalTerminateHandler);
bsg_g_installed = 0;
bsg_g_installed.store(false, std::memory_order_relaxed);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// BSG_KSCrashSentry_CPPException_Private.h
// Bugsnag
//
// Created by Meiyalagan Ramadurai on 27/08/26.
// Copyright © 2026 Bugsnag Inc. All rights reserved.
//

#ifndef BSG_KSCrashSentry_CPPException_Private_h
#define BSG_KSCrashSentry_CPPException_Private_h

#include <stdint.h>

#ifdef __cplusplus

static constexpr int BSG_KSCPPExceptionStackTraceCapacity = 30;

struct BSG_KSCPPExceptionThreadState {
bool captureNextStackTrace = true;
uintptr_t stackTrace[BSG_KSCPPExceptionStackTraceCapacity] = {};
int stackTraceCount = 0;
};

struct BSG_KSCPPExceptionStackTraceView {
uintptr_t *stackTrace;
int stackTraceLength;
};

inline BSG_KSCPPExceptionThreadState &bsg_kscrashsentry_cppExceptionThreadState(void) {
static thread_local BSG_KSCPPExceptionThreadState state;
return state;
}

inline BSG_KSCPPExceptionStackTraceView bsg_kscrashsentry_cppExceptionStackTraceView(
BSG_KSCPPExceptionThreadState &state, int framesToSkip) {
if (framesToSkip < 0) {
framesToSkip = 0;
}
const int stackTraceLength = state.stackTraceCount > framesToSkip
? state.stackTraceCount - framesToSkip : 0;
return {
stackTraceLength > 0 ? state.stackTrace + framesToSkip : nullptr,
stackTraceLength,
};
}

#endif __cplusplus

#endif // BSG_KSCrashSentry_CPPException_Private_h
80 changes: 80 additions & 0 deletions Tests/KSCrashTests/KSCrashSentry_CPPException_Tests.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// KSCrashSentry_CPPException_Tests.mm
// Bugsnag
//
// Created by Meiyalagan Ramadurai on 27/08/26.
// Copyright © 2026 Bugsnag Inc. All rights reserved.
//

#import <XCTest/XCTest.h>

#include "BSG_KSCrashSentry_CPPException_Private.h"
Comment thread
SB-meiyalaganr marked this conversation as resolved.

#include <atomic>
#include <thread>

@interface KSCrashSentry_CPPException_Tests : XCTestCase
@end

@implementation KSCrashSentry_CPPException_Tests

- (void)tearDown {
bsg_kscrashsentry_cppExceptionThreadState() = BSG_KSCPPExceptionThreadState{};
[super tearDown];
}

- (void)testExceptionStateIsIsolatedByThread {
BSG_KSCPPExceptionThreadState &mainState = bsg_kscrashsentry_cppExceptionThreadState();
mainState.captureNextStackTrace = false;
mainState.stackTrace[0] = 0xa;
mainState.stackTraceCount = 1;

std::atomic<bool> workerSawDefaults{false};
std::thread worker([&] {
BSG_KSCPPExceptionThreadState &workerState =
bsg_kscrashsentry_cppExceptionThreadState();
workerSawDefaults.store(workerState.captureNextStackTrace &&
workerState.stackTrace[0] == 0 &&
workerState.stackTraceCount == 0);
workerState.captureNextStackTrace = false;
workerState.stackTrace[0] = 0xb;
workerState.stackTraceCount = 2;
});
worker.join();

XCTAssertTrue(workerSawDefaults.load());
XCTAssertFalse(mainState.captureNextStackTrace);
XCTAssertEqual(mainState.stackTrace[0], (uintptr_t)0xa);
XCTAssertEqual(mainState.stackTraceCount, 1);
}

- (void)testStackTraceViewSkipsInternalFrames {
BSG_KSCPPExceptionThreadState state;
state.stackTraceCount = 4;
for (int index = 0; index < state.stackTraceCount; index++) {
state.stackTrace[index] = (uintptr_t)(100 + index);
}

BSG_KSCPPExceptionStackTraceView view =
bsg_kscrashsentry_cppExceptionStackTraceView(state, 2);

XCTAssertEqual(view.stackTraceLength, 2);
XCTAssertEqual(view.stackTrace, state.stackTrace + 2);
XCTAssertEqual(view.stackTrace[0], (uintptr_t)102);
}

- (void)testStackTraceViewSafelyHandlesShortAndInvalidCounts {
for (int count : {-1, 0, 1, 2}) {
BSG_KSCPPExceptionThreadState state;
state.stackTraceCount = count;

BSG_KSCPPExceptionStackTraceView view =
bsg_kscrashsentry_cppExceptionStackTraceView(state, 2);

XCTAssertEqual(view.stackTraceLength, 0, @"count = %d", count);
XCTAssertEqual(view.stackTrace, nullptr, @"count = %d", count);
}
}

@end

20 changes: 20 additions & 0 deletions features/fixtures/shared/scenarios/CxxBareThrowScenario.mm
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,23 @@ - (void)run {
}

@end

@interface CxxBareThrowAfterCatchScenario : CxxBareThrowScenario
@end

@implementation CxxBareThrowAfterCatchScenario

- (void)run {
// A caught exception must not supply the trace for a later bare throw.
try {
[self throwCaughtException];
} catch (const std::exception &) {
}
[super run];
}

- (void)throwCaughtException __attribute__((noreturn)) {
throw std::runtime_error("Previously caught exception");
}

@end
Loading
Loading