Skip to content

fix: resolve EXC_BAD_ACCESS in SentryTracer/SentryNetworkTracker span lifecycle#8058

Open
itaybre wants to merge 8 commits into
mainfrom
fix/exc-bad-access-tracer-network-span-lifecycle
Open

fix: resolve EXC_BAD_ACCESS in SentryTracer/SentryNetworkTracker span lifecycle#8058
itaybre wants to merge 8 commits into
mainfrom
fix/exc-bad-access-tracer-network-span-lifecycle

Conversation

@itaybre

@itaybre itaybre commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Description

Fix two crash paths in the SentryTracerSentryNetworkTracker call chain that cause EXC_BAD_ACCESS crashes. This is the most frequent crash pattern in SDK-CRASHES-COCOA-5 (43/100 sampled events), affecting 7 customer projects across SDK versions 8.36.0–9.9.0 with ~140K events in the last 90 days.

Root causes and fixes

1. TOCTOU race in SentryTracer.canBeFinishedhasUnfinishedChildSpansToWaitFor was evaluated outside @synchronized(self), so two concurrent threads could both observe "no unfinished children" and race into finishInternal, operating on deallocated state. Moved the check inside the synchronized block.

2. Volatile currentRequest re-readsSentryTracePropagation.addBaggageHeader: and SentryNetworkTracker.urlSessionTaskResume: accessed sessionTask.currentRequest multiple times without retaining. The property can return a freed object if the task completes on another thread between reads (CFURLRequestSetHTTPRequestBody crash). Both methods now snapshot the request once into a local variable.

3. Weak span dangling refs in TTD callbackSentryTimeToDisplayTracker's finishCallback accessed weak initialDisplaySpan/fullDisplaySpan properties repeatedly without strongifying. The spans could be zeroed mid-callback if the tracer's children are released concurrently. Now strongified into locals at callback entry with nil guards.

How tested

  • make format — clean
  • make analyze — clean
  • make build-ios — succeeds
  • make test-ios ONLY_TESTING=SentryTests/SentryTracerTests — 86 tests pass
  • make test-ios ONLY_TESTING=SentryTests/SentryTracePropagationTests,SentryTests/SentryNetworkTrackerTests,SentryTests/SentryTimeToDisplayTrackerTest — 100 tests pass

Closes #8012

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.22222% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 87.203%. Comparing base (11b0eef) to head (ad1d81a).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
Sources/Sentry/SentryTracePropagation.m 92.307% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##              main     #8058       +/-   ##
=============================================
- Coverage   87.306%   87.203%   -0.104%     
=============================================
  Files          554       559        +5     
  Lines        31986     32172      +186     
  Branches     13139     13146        +7     
=============================================
+ Hits         27926     28055      +129     
- Misses        4012      4069       +57     
  Partials        48        48               
Files with missing lines Coverage Δ
Sources/Sentry/SentryNetworkTracker.m 97.759% <100.000%> (ø)
Sources/Sentry/SentryTimeToDisplayTracker.m 98.979% <100.000%> (+0.032%) ⬆️
Sources/Sentry/SentryTracer.m 99.297% <100.000%> (+1.639%) ⬆️
Sources/Sentry/SentryTracePropagation.m 75.862% <92.307%> (-5.957%) ⬇️

... and 21 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 11b0eef...ad1d81a. Read the comment docs.

itaybre added 2 commits June 29, 2026 16:28
… lifecycle

Fix two crash paths in the SentryTracer → SentryNetworkTracker call chain
that cause use-after-free crashes (SDK-CRASHES-COCOA-5, ~140K events/90d):

1. TOCTOU race in canBeFinished: hasUnfinishedChildSpansToWaitFor was
   checked outside @synchronized(self), allowing concurrent threads to
   both see "no unfinished children" and call finishInternal. Moved the
   check inside the synchronized block.

2. Volatile currentRequest re-reads: SentryTracePropagation and
   SentryNetworkTracker accessed sessionTask.currentRequest multiple
   times without retaining. The property can return a freed object if
   the task completes on another thread between reads. Snapshot the
   request once into a local variable in both methods.

3. Weak span dangling refs in TTD callback: SentryTimeToDisplayTracker's
   finishCallback accessed weak initialDisplaySpan/fullDisplaySpan
   properties repeatedly. Strongify into locals at callback entry with
   nil guards.

Closes #8012
Cover the three crash paths fixed in the previous commit:

- SentryTracerTests: concurrent child span finish racing with
  tracer.finish() to verify canBeFinished atomicity
- SentryNetworkTrackerTests: concurrent resume + setState on the
  same task, and resume after task already completed
- SentryTimeToDisplayTrackerTest: concurrent tracer finish with
  child span operations, and finish with no full display span
- SentryTracePropagationTests: addBaggageHeader with nil
  currentRequest (task with no request set)
@itaybre itaybre force-pushed the fix/exc-bad-access-tracer-network-span-lifecycle branch from ad1d81a to 2712c25 Compare June 29, 2026 19:50
@itaybre itaybre added the ready-to-merge Use this label to trigger all PR workflows label Jun 29, 2026
@sentry

sentry Bot commented Jun 29, 2026

Copy link
Copy Markdown

📲 Install Builds

iOS

🔗 App Name App ID Version Configuration
SDK-Size io.sentry.sample.SDK-Size 9.21.0 (1) Release

⚙️ sentry-cocoa Build Distribution Settings

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1232.90 ms 1262.83 ms 29.93 ms
Size 24.14 KiB 1.23 MiB 1.20 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
52f26a1 1227.29 ms 1261.50 ms 34.21 ms
8c39e1a 1223.40 ms 1255.42 ms 32.02 ms
8d75f5a 1223.90 ms 1255.94 ms 32.04 ms
943b250 1214.69 ms 1257.32 ms 42.63 ms
4c437ea 1217.19 ms 1250.79 ms 33.60 ms
8180609 1214.67 ms 1243.36 ms 28.69 ms
2c9ee54 1226.87 ms 1251.10 ms 24.23 ms
1887b2e 1218.51 ms 1243.06 ms 24.55 ms
1c5ecda 1219.35 ms 1253.76 ms 34.41 ms
e983818 1214.41 ms 1245.20 ms 30.80 ms

App size

Revision Plain With Sentry Diff
52f26a1 24.14 KiB 1.19 MiB 1.17 MiB
8c39e1a 24.14 KiB 1.22 MiB 1.20 MiB
8d75f5a 24.14 KiB 1.17 MiB 1.15 MiB
943b250 24.14 KiB 1.17 MiB 1.15 MiB
4c437ea 24.14 KiB 1.18 MiB 1.15 MiB
8180609 24.14 KiB 1.16 MiB 1.13 MiB
2c9ee54 24.14 KiB 1.17 MiB 1.15 MiB
1887b2e 24.14 KiB 1.15 MiB 1.13 MiB
1c5ecda 24.14 KiB 1.15 MiB 1.12 MiB
e983818 24.14 KiB 1.22 MiB 1.20 MiB

Previous results on branch: fix/exc-bad-access-tracer-network-span-lifecycle

Startup times

Revision Plain With Sentry Diff
a6c6416 1220.39 ms 1243.80 ms 23.41 ms
1119cfd 1232.30 ms 1269.40 ms 37.10 ms
6fdbc97 1212.43 ms 1248.49 ms 36.06 ms

App size

Revision Plain With Sentry Diff
a6c6416 24.14 KiB 1.22 MiB 1.20 MiB
1119cfd 24.14 KiB 1.22 MiB 1.20 MiB
6fdbc97 24.14 KiB 1.22 MiB 1.20 MiB

@itaybre itaybre marked this pull request as ready for review July 1, 2026 08:33
Comment thread Sources/Sentry/SentryTracer.m Outdated
itaybre added 2 commits July 1, 2026 13:07
Moving hasUnfinishedChildSpansToWaitFor inside @synchronized(self)
introduced an ABBA deadlock: canBeFinished takes self then _children,
while finishTracer holds _children and re-enters canBeFinished via
spanFinished, waiting on self.

The original TOCTOU was already guarded by the double-check lock in
finishTracer (lines 580-588), so the move was unnecessary.
Comment thread Sources/Sentry/SentryTimeToDisplayTracker.m Outdated

@philprime philprime left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread Sources/Sentry/SentryNetworkTracker.m
@itaybre itaybre enabled auto-merge (squash) July 2, 2026 14:05

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6435c68. Configure here.

Comment thread Sources/Sentry/SentryTimeToDisplayTracker.m Outdated
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🚨 Detected changes in high risk code 🚨

High-risk code can easily blow up and is hard to test. We had severe bugs in the past. Be extra careful when changing these files, and have an extra careful look at these:

  • Sources/Sentry/SentryNetworkTracker.m

@philipphofmann philipphofmann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if this PR fixes the problem. If the root cause is unsynchronized concurrent mutation of the task, this PR narrows the window but doesn't remove it, if I'm not mistaken.

Comment on lines +18 to +20
// Snapshot currentRequest once — the property is volatile and can become a zombie
// between repeated accesses if the task completes on another thread.
NSURLRequest *request = sessionTask.currentRequest;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: The SentryTracePropagation.addBaggageHeader only gets called from the SentryNetworkTracker.urlSessionTaskResume which already snapshots the sessionTask.currentRequest before the @synchronized(sessionTask) {. We could also pass the already snapshotted currentRequest as a parameter to the SentryTracePropagation.addBaggageHeader.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea


// MARK: - Weak span safety in finishCallback (issue #8012)

func testFinishCallback_ConcurrentTracerFinish_DoesNotCrash() throws {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: I ran these two tests repeatedly (1000 times) without the fix in this PR, and they didn't crash. Are you sure they reproduce the crash? Am I missing something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It failed for me when I ran, but seems to be non deterministic (or a later change broke it).

And it was more of an integration test. I will add a different test here

Comment on lines +126 to +129
// Snapshot currentRequest once — the property is volatile and can return a freed
// object if the task completes on another thread between repeated accesses.
NSURLRequest *currentRequest = sessionTask.currentRequest;
NSURL *url = currentRequest.URL;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: Snapshotting this could reduce the chance of returning a freed object, but if the root cause is multiple threads modifying this, this crash will happen again. Or am I missing something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are snapshotting a copy, so the possibility of this is lower.

@itaybre

itaybre commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

I'm unsure if this PR fixes the problem. If the root cause is unsynchronized concurrent mutation of the task, this PR narrows the window but doesn't remove it, if I'm not mistaken.

I believe this is one of the limitation of swizzling native frameworks, we don't know how apple may access / modify the tasks so we cant do much aside from protecting every time we access the object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved ready-to-merge Use this label to trigger all PR workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EXC_BAD_ACCESS in SentryTracer/SentryNetworkTracker span lifecycle

3 participants