Skip to content

Commit 2719ce6

Browse files
fix: A crash for user interaction transactions (#3036)
Finishing multiple spans on different threads that cancel and reschedule the idle timeout for user interaction transactions could lead to EXC_BAD_ACCESS crashes. This is fixed now by synchronizing the access to the idleTimeout block. Fixes GH-3002
1 parent a54df45 commit 2719ce6

File tree

3 files changed

+37
-21
lines changed

3 files changed

+37
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixed
66

77
- Fix crashes in profiling serialization race condition (#3018)
8+
- Fix a crash for user interaction transactions (#3036)
89

910
## 8.7.1
1011

Sources/Sentry/SentryTracer.m

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ @implementation SentryTracer {
7575
NSMutableArray<id<SentrySpan>> *_children;
7676
BOOL _startTimeChanged;
7777
NSDate *_originalStartTimestamp;
78+
NSObject *_idleTimeoutLock;
7879

7980
#if SENTRY_HAS_UIKIT
8081
NSUInteger initTotalFrames;
@@ -125,6 +126,7 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti
125126

126127
appStartMeasurement = [self getAppStartMeasurement];
127128

129+
_idleTimeoutLock = [[NSObject alloc] init];
128130
if ([self hasIdleTimeout]) {
129131
[self dispatchIdleTimeout];
130132
}
@@ -164,26 +166,28 @@ - (nullable SentryTracer *)tracer
164166

165167
- (void)dispatchIdleTimeout
166168
{
167-
if (_idleTimeoutBlock != nil) {
168-
[_configuration.dispatchQueueWrapper dispatchCancel:_idleTimeoutBlock];
169-
}
170-
__weak SentryTracer *weakSelf = self;
171-
_idleTimeoutBlock = [_configuration.dispatchQueueWrapper createDispatchBlock:^{
172-
if (weakSelf == nil) {
173-
SENTRY_LOG_DEBUG(@"WeakSelf is nil. Not doing anything.");
174-
return;
169+
@synchronized(_idleTimeoutLock) {
170+
if (_idleTimeoutBlock != NULL) {
171+
[_configuration.dispatchQueueWrapper dispatchCancel:_idleTimeoutBlock];
172+
}
173+
__weak SentryTracer *weakSelf = self;
174+
_idleTimeoutBlock = [_configuration.dispatchQueueWrapper createDispatchBlock:^{
175+
if (weakSelf == nil) {
176+
SENTRY_LOG_DEBUG(@"WeakSelf is nil. Not doing anything.");
177+
return;
178+
}
179+
[weakSelf finishInternal];
180+
}];
181+
182+
if (_idleTimeoutBlock == NULL) {
183+
SENTRY_LOG_WARN(@"Couldn't create idle time out block. Can't schedule idle timeout. "
184+
@"Finishing transaction");
185+
// If the transaction has no children, the SDK will discard it.
186+
[self finishInternal];
187+
} else {
188+
[_configuration.dispatchQueueWrapper dispatchAfter:_configuration.idleTimeout
189+
block:_idleTimeoutBlock];
175190
}
176-
[weakSelf finishInternal];
177-
}];
178-
179-
if (_idleTimeoutBlock == NULL) {
180-
SENTRY_LOG_WARN(@"Couldn't create idle time out block. Can't schedule idle timeout. "
181-
@"Finishing transaction");
182-
// If the transaction has no children, the SDK will discard it.
183-
[self finishInternal];
184-
} else {
185-
[_configuration.dispatchQueueWrapper dispatchAfter:_configuration.idleTimeout
186-
block:_idleTimeoutBlock];
187191
}
188192
}
189193

@@ -199,8 +203,10 @@ - (BOOL)isAutoGeneratedTransaction
199203

200204
- (void)cancelIdleTimeout
201205
{
202-
if ([self hasIdleTimeout]) {
203-
[_configuration.dispatchQueueWrapper dispatchCancel:_idleTimeoutBlock];
206+
@synchronized(_idleTimeoutLock) {
207+
if ([self hasIdleTimeout]) {
208+
[_configuration.dispatchQueueWrapper dispatchCancel:_idleTimeoutBlock];
209+
}
204210
}
205211
}
206212

Tests/SentryTests/Transaction/SentryTracerTests.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,15 @@ class SentryTracerTests: XCTestCase {
211211

212212
XCTAssertFalse(fixture.timerWrapper.overrides.timer.isValid)
213213
}
214+
215+
func testDeadlineTimer_MultipleSpansFinishedInParallel() {
216+
let sut = fixture.getSut(idleTimeout: 0.01, dispatchQueueWrapper: SentryDispatchQueueWrapper())
217+
218+
testConcurrentModifications(writeWork: { _ in
219+
let child = sut.startChild(operation: self.fixture.transactionOperation)
220+
child.finish()
221+
})
222+
}
214223

215224
func testFinish_CheckDefaultStatus() {
216225
let sut = fixture.getSut()

0 commit comments

Comments
 (0)