You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fatal App Hangs where the main thread is blocked on a per-NSURLSessionTask lock inside Sentry's URLSession swizzle. Sentry's app-hang detector captures the main-thread stack after it has been unresponsive ≥ 2s (reported as Fatal App Hang – Fully Blocked). The hang originates from -[SentryNetworkTracker urlSessionTask:setState:] running synchronously on the main thread during -[NSURLSessionTask cancel], then blocking on @synchronized(sessionTask) in addBreadcrumbForSessionTask:.
Scoped to SDK 9.18.0: ~398 events / 3 users in the last 90 days within SDK-CRASHES-COCOA-MMJ.
Stack Traces
Main thread (blocked acquiring the per-task lock):
The app cancels a Combine chain on the main thread → -[NSURLSessionTask cancel] transitions the task to Canceling → our swizzled setState: runs the tracker synchronously on the main thread → it blocks in objc_sync_enter.
Root Cause Analysis
The setState: swizzle executes tracker work synchronously on whatever thread calls setState: (SentrySwizzleWrapperHelper.m:108-113), with no hop to a background queue. Combine-driven cancellation calls -[NSURLSessionTask cancel] on the main thread, so the entire tracking path runs there.
urlSessionTask:setState: reaches addBreadcrumbForSessionTask:, which takes @synchronized(sessionTask) at SentryNetworkTracker.m:487. This is the leaf objc_sync_enter in the stack — the main thread waits > 2s for a lock on the NSURLSessionTask, tripping the app-hang watchdog.
The same task is locked via @synchronized(sessionTask) in five places in the tracker — urlSessionTaskResume: (SentryNetworkTracker.m:157, holds the lock across trace-propagation/baggage work), urlSessionTask:setState: (:274), addBreadcrumbForSessionTask: (:487), captureResponseDetails: (:627), and captureRequestDetails: (:670). A com.apple.NSURLSession-work thread mutating the same task under one of these sections contends the main thread's acquisition at :487.
Two contributing factors specific to this shape:
Lock taken on the main thread's critical path. Because the swizzle is synchronous, cancel on the main thread must acquire the per-task lock before it can finish. Any concurrent holder stalls the UI.
This is distinct from the previously fixed main-thread network hang (#3277, 8.11.0), which was a re-entrant deadlock via captureFailedRequests → dispatchSyncOnMainQueue. The 9.18.0 signature blocks in addBreadcrumbForSessionTask: on the per-task lock instead.
Potential fixes to evaluate:
Remove or narrow the SentryNetworkTracker.m:487 lock (associated-object access is already thread-safe; guarding a single read with @synchronized(sessionTask) appears unnecessary).
Stop running urlSessionTask:setState: tracking work synchronously on the calling thread when it is the main thread (hop to a background queue), addressing the whole class of main-thread network hangs.
Environment
SDK version: 9.18.0
OS versions: iOS only — predominantly iOS 26.5 / 26.5.1, also seen on 18.7.x
Problem
Fatal App Hangs where the main thread is blocked on a per-
NSURLSessionTasklock inside Sentry's URLSession swizzle. Sentry's app-hang detector captures the main-thread stack after it has been unresponsive ≥ 2s (reported as Fatal App Hang – Fully Blocked). The hang originates from-[SentryNetworkTracker urlSessionTask:setState:]running synchronously on the main thread during-[NSURLSessionTask cancel], then blocking on@synchronized(sessionTask)inaddBreadcrumbForSessionTask:.Scoped to SDK 9.18.0: ~398 events / 3 users in the last 90 days within SDK-CRASHES-COCOA-MMJ.
Stack Traces
Main thread (blocked acquiring the per-task lock):
The app cancels a Combine chain on the main thread →
-[NSURLSessionTask cancel]transitions the task toCanceling→ our swizzledsetState:runs the tracker synchronously on the main thread → it blocks inobjc_sync_enter.Root Cause Analysis
The
setState:swizzle executes tracker work synchronously on whatever thread callssetState:(SentrySwizzleWrapperHelper.m:108-113), with no hop to a background queue. Combine-driven cancellation calls-[NSURLSessionTask cancel]on the main thread, so the entire tracking path runs there.urlSessionTask:setState:reachesaddBreadcrumbForSessionTask:, which takes@synchronized(sessionTask)atSentryNetworkTracker.m:487. This is the leafobjc_sync_enterin the stack — the main thread waits > 2s for a lock on theNSURLSessionTask, tripping the app-hang watchdog.The same task is locked via
@synchronized(sessionTask)in five places in the tracker —urlSessionTaskResume:(SentryNetworkTracker.m:157, holds the lock across trace-propagation/baggage work),urlSessionTask:setState:(:274),addBreadcrumbForSessionTask:(:487),captureResponseDetails:(:627), andcaptureRequestDetails:(:670). Acom.apple.NSURLSession-workthread mutating the same task under one of these sections contends the main thread's acquisition at:487.Two contributing factors specific to this shape:
Lock taken on the main thread's critical path. Because the swizzle is synchronous,
cancelon the main thread must acquire the per-task lock before it can finish. Any concurrent holder stalls the UI.The
:487lock is unconditional on iOS. It sits inside#if SENTRY_TARGET_REPLAY_SUPPORTEDbut only guards a singleobjc_getAssociatedObjectread, and it is reached whenever breadcrumbs are enabled (the default) — independent of whether Session Replay / network-details capture is on. This per-task lock acquisition was introduced by the Session-Replay network-details feature (commitc5c7a299e, PRs feat(network-details): New swizzling to capture response bodies for session replay #7584 / feat(network-details): Hook up request/response capture in SentryNetworkTracker #7588 / feat(network-details): Extract network details data to Session Replay #7590, April 2026), which also added the sibling@synchronized(sessionTask)sections incaptureRequestDetails:/captureResponseDetails:, widening the contention window on the per-task lock.This is distinct from the previously fixed main-thread network hang (#3277, 8.11.0), which was a re-entrant deadlock via
captureFailedRequests→dispatchSyncOnMainQueue. The 9.18.0 signature blocks inaddBreadcrumbForSessionTask:on the per-task lock instead.Potential fixes to evaluate:
SentryNetworkTracker.m:487lock (associated-object access is already thread-safe; guarding a single read with@synchronized(sessionTask)appears unnecessary).urlSessionTask:setState:tracking work synchronously on the calling thread when it is the main thread (hop to a background queue), addressing the whole class of main-thread network hangs.Environment