Skip to content

Fatal App Hang in SentryNetworkTracker urlSessionTask:setState: main-thread lock #8355

Description

@NinjaLikesCheez

Problem

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):

__ulock_wait2
_os_unfair_lock_lock_slow
objc_sync_enter
-[SentryNetworkTracker addBreadcrumbForSessionTask:]
-[SentryNetworkTracker urlSessionTask:setState:]
__52+[SentrySwizzleWrapperHelper swizzleURLSessionTask:]_block_invoke_2.28
-[NSURLSessionTask cancel]
swift_task_cancelImpl
AnyCancellable.cancel
Publishers.Concatenate.Inner.cancel
Publishers.ReceiveOn.Inner.cancel
Publishers.HandleEvents.Inner.cancel
Subscribers.Sink.cancel
AnyCancellable.cancel
swift::runJobInEstablishedExecutorContext

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:

  1. 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.

  2. The :487 lock is unconditional on iOS. It sits inside #if SENTRY_TARGET_REPLAY_SUPPORTED but only guards a single objc_getAssociatedObject read, 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 (commit c5c7a299e, 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 in captureRequestDetails: / 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 captureFailedRequestsdispatchSyncOnMainQueue. 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
  • Devices: modern iPhones (iPhone18,x / 17,x / 16,x / 15,x / 14,x)
  • Mechanism: AppHang (Fatal App Hang – Fully Blocked); handled: no
  • Sentry issue: SDK-CRASHES-COCOA-MMJ
  • Events (9.18.0, 90d): ~398 events / 3 users (issue lifetime: 9,825 events / 45 users, first seen 2025-08-22)

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions