[ Web Proxy ]
URL:
Viewing: https://github-redirect.dependabot.com/getsentry/sentry-cocoa/issues/7155 [Back]  [Original]

fix: Crash in `SentryFramesTracker.removeListener` by denrase Pull Request #7155 getsentry/sentry-cocoa GitHub
Skip to content

Navigation Menu

Repository navigation

More items

fix: Crash in SentryFramesTracker.removeListener - #7155

Merged
denrase merged 17 commits into
mainfrom
fix/frames-tracker-remove-listner-crash
Jan 20, 2026
Merged

denrase merged 17 commits into
mainfrom
fix/frames-tracker-remove-listner-crash

Conversation

@denrase [@denrase]

Copy link
Copy Markdown
Collaborator

Description

Mutate listeners in SentryFramesTracker only on main queue.

Access to listeners was moved to main thread dispatch instead of locks, but one lock was probably forgotten. The reason why this was changed is added as a comment in code, so because of this we do not use the locking anymore.

The DisplayLink callback always runs on the main thread. We dispatch this to the main thread instead to avoid using locks in the DisplayLink callback.

If this issue should remain even after this fix, we can still additionally add locks/synchronisation around listeners access.

Motivation and Context

Closes https://linear.app/getsentry/issue/COCOA-869/sentryframestracker-removelistener-crash

How did you test it?

Unfortunately, I could not reproduce the crash, but the sync was only in one place, suggesting it was forgotten to move this to main thread access only.

Checklist

You have to check all boxes before merging:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.
[Go]
@denrase [@denrase] denrase added the ready-to-merge (deprecated) Use run-full-ci instead. label Jan 12, 2026
denrase marked this pull request as ready for review January 12, 2026 12:50
@denrase [@denrase] denrase changed the title fix: [SentryFramesTracker removeListener:] crash fix [SentryFramesTracker removeListener:] crash Jan 12, 2026
@denrase [@denrase] denrase changed the title fix [SentryFramesTracker removeListener:] crash Fix [SentryFramesTracker removeListener:] crash Jan 12, 2026

denrase commented Jan 12, 2026

Copy link
Copy Markdown
Collaborator Author

@philprime Due to the comment in the code, moved the last remaining lock to main thread dispatch instead.

[Go]

codecov Bot commented Jan 12, 2026 •
edited
Loading

Copy link
Copy Markdown

Codecov Report

All modified and coverable lines are covered by tests.
Project coverage is 85.091%. Comparing base (d8db577) to head (fc620db).
Report is 8 commits behind head on main.
All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph [Impacted file tree graph]

@@              Coverage Diff              @@
##              main     #7155       +/-   ##
=============================================
+ Coverage   85.062%   85.091%   +0.029%     
=============================================
  Files          468       468               
  Lines        28305     28320       +15     
  Branches     12532     12547       +15     
=============================================
+ Hits         24077     24098       +21     
+ Misses        4183      4181        -2     
+ Partials        45        41        -4     
Files with missing lines Coverage
...egrations/FramesTracking/SentryFramesTracker.swift 100.000% <100.000%> ()

... and 9 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

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

[Go]

@philprime [@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.

[Hide comment]

LGTM, but just to be certain: Instead of using a lock we are going all-in on dispatch on the main thread, because we might be locking the main thread?

[Go]
Comment thread CHANGELOG.md Resolved

github-actions Bot commented Jan 12, 2026 •
edited
Loading

Copy link
Copy Markdown
Contributor

Performance metrics

Plain With Sentry Diff
Startup time 1211.83 ms 1234.10 ms 22.27 ms
Size 24.14 KiB 1.06 MiB 1.04 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
93ef486 1220.22 ms 1244.96 ms 24.74 ms
3bf0d3f 1202.12 ms 1237.23 ms 35.11 ms
17b7e88 1223.04 ms 1253.48 ms 30.43 ms
bb418da 1227.60 ms 1265.90 ms 38.30 ms
dbfeb41 1215.17 ms 1237.41 ms 22.23 ms
d8db577 1206.83 ms 1244.39 ms 37.56 ms
a424cf3 1231.36 ms 1259.62 ms 28.25 ms
f84c826 1216.38 ms 1241.98 ms 25.60 ms
21cd5ba 1218.68 ms 1255.54 ms 36.86 ms
db9e223 1193.69 ms 1213.56 ms 19.87 ms

App size

Revision Plain With Sentry Diff
93ef486 24.14 KiB 1.06 MiB 1.04 MiB
3bf0d3f 24.14 KiB 1.04 MiB 1.02 MiB
17b7e88 24.14 KiB 1.06 MiB 1.04 MiB
bb418da 24.14 KiB 1.04 MiB 1.02 MiB
dbfeb41 24.14 KiB 1.04 MiB 1.02 MiB
d8db577 24.14 KiB 1.06 MiB 1.04 MiB
a424cf3 24.14 KiB 1.06 MiB 1.04 MiB
f84c826 24.14 KiB 1.04 MiB 1.02 MiB
21cd5ba 24.14 KiB 1.04 MiB 1.02 MiB
db9e223 24.14 KiB 1.06 MiB 1.03 MiB

Previous results on branch: fix/frames-tracker-remove-listner-crash

Startup times

Revision Plain With Sentry Diff
c6fdfe7 1219.85 ms 1250.46 ms 30.61 ms
fc7e87f 1219.17 ms 1239.91 ms 20.74 ms
f8aca10 1199.12 ms 1220.77 ms 21.64 ms
af0d84f 1207.13 ms 1247.29 ms 40.16 ms
80ec048 1206.65 ms 1240.59 ms 33.94 ms
8bc0e88 1227.00 ms 1257.88 ms 30.88 ms
e7f5931 1188.24 ms 1208.34 ms 20.10 ms

App size

Revision Plain With Sentry Diff
c6fdfe7 24.14 KiB 1.04 MiB 1.02 MiB
fc7e87f 24.14 KiB 1.04 MiB 1.02 MiB
f8aca10 24.14 KiB 1.04 MiB 1.02 MiB
af0d84f 24.14 KiB 1.04 MiB 1.02 MiB
80ec048 24.14 KiB 1.06 MiB 1.04 MiB
8bc0e88 24.14 KiB 1.04 MiB 1.02 MiB
e7f5931 24.14 KiB 1.06 MiB 1.04 MiB
[Go]

denrase commented Jan 12, 2026

Copy link
Copy Markdown
Collaborator Author

@philprime Not because of locking the main thread, but the code base was moving away from locks, with the reasoning that DisplayLink is always calling on the main thread anyway, so this PR completes this transition.

But there's the catch that I was not able to reproduce the crash from the issue, but basically it was possible to clear the listeners from a non-main thread, while adding/removing individual listeners was always being done on the main-thread.

[Go]
@denrase [@denrase] denrase changed the title Fix [SentryFramesTracker removeListener:] crash Resolve crash in caused by calling SentryFramesTracker.removeListener(_:) Jan 12, 2026

github-actions Bot commented Jan 12, 2026 •
edited
Loading

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

Patch (bug fixes)

Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features

Metrics

  • Add public API to collect count, distribution and gauge by philprime in #6957
  • Add implementation for metrics envelope item by philprime in #6960
  • Add integration with installation by SDK by philprime in #6956

Other

  • Add metrics flushing support to SentrySDK.flush() by philprime in #7182
  • Add isiOSAppOnVisionOS, isiOSAppOnMac, isMacCatalystApp to device context by philprime in #6939

Bug Fixes

  • (logs) Use sendDefaultPii and span_id for attributes by philprime in #7055
  • Crash in SentryFramesTracker.removeListener by denrase in #7155
  • Fix incorrect variable assignment for 'sampled' key by xjshi in #7120
  • Mark dark theme deprecated by noahsmartin in #7114
  • Update raw_description in runtime context for Mac Catalyst App by philprime in #7082
  • Use correct parsing for stackframes by noahsmartin in #6908
  • Transport correctly handling 4xx and 5xx by dfed in #6618

Build / dependencies / internal

Deps

  • Bump fastlane from 2.230.0 to 2.231.0 by dependabot in #7203
  • Bump actions/upload-artifact from 5 to 6 by dependabot in #7206
  • Bump actions/setup-node from 6.1.0 to 6.2.0 by dependabot in #7207
  • Bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.18.3 to 2.19.0 by dependabot in #7205
  • Bump ruby/setup-ruby from 1.279.0 to 1.281.0 by dependabot in #7160
  • Bump getsentry/craft from 2.18.1 to 2.18.3 by dependabot in #7161
  • Bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.18.1 to 2.18.3 by dependabot in #7159
  • Bump ruby/setup-ruby from 1.276.0 to 1.279.0 by dependabot in #7117
  • Bump mikepenz/action-junit-report from 6.0.1 to 6.1.0 by dependabot in #7116
  • Update swiftlint version by github-actions in #7109
  • Bump ruby/setup-ruby from 1.275.0 to 1.276.0 by dependabot in #7103
  • Bump codecov/test-results-action from 1.1.1 to 1.2.1 by itaybre in #7087
  • Bump ruby/setup-ruby from 1.270.0 to 1.275.0 by itaybre in #7088
  • Bump peter-evans/create-pull-request from 7.0.11 to 8.0.0 by dependabot in #7084
  • Bump actions/download-artifact from 6 to 7 by dependabot in #7048
  • Bump aws-sdk-s3 from 1.205.0 to 1.208.0 by dependabot in #7074
  • Bump ruby/setup-ruby from 1.269.0 to 1.270.0 by dependabot in #7049
  • Update clang-format version by github-actions in #7056
  • Bump actions/cache from 4 to 5 by dependabot in #7052
  • Bump actions/upload-artifact from 5 to 6 by dependabot in #7050
  • Bump codecov/codecov-action from 5.5.1 to 5.5.2 by dependabot in #7051

Other

  • (dx) Add structured Makefile with usage description by philprime in #7129
  • (release) Switch from action-prepare-release to Craft (minimal) by BYK in #7153
  • Use pull_request_target for changelog preview by BYK in #7190
  • Allow whitespaces on Xcode path when executing make init by itaybre in #7193
  • Add temporary macro for Swift migration tracking of types erased by itaybre in #7178
  • Renames SentrySpan class to SentrySpanInternal by itaybre in #7172
  • Convert SentryNetworkTrackingIntegration to Swift by itaybre in #7106
  • Update iOS test destination OS version to 18.5 in fast-pr-checks workflow by itaybre in #7168
  • Fix typos in comments in multiple files v2 by philipphofmann in #7139
  • Run visionOS tests on Cirrus Runners + Boot simulator by itaybre in #7147
  • Skip jobs/steps that require secrets for non contributors by itaybre in #7124
  • Add attributable protocol for typed attribute values by philprime in #7077
  • Allow alpha releases on RNSentry.podspec for Cross Platform Test by itaybre in #7130
  • Remove swift5.9 checks by itaybre in #7098
  • Remove duplicate file in project by itaybre in #7093
  • Convert SentryMetricKitIntegration to Swift by noahsmartin in #7076
  • Removes HybridSDK subspec by itaybre in #7019
  • Move testRemoveImageFromTail to flaky plan by itaybre in #7041
  • Use at least xcode 16 for all jobs by itaybre in #7012
  • Cleanup file filter for required files modified by itaybre in #7031
  • Remove assembly workflow files from UI test filter by itaybre in #7030
  • Bumps macOS-14 runner to macOS-15 by itaybre in #7029
  • Ensure required simulators are loaded for all platforms by itaybre in #7022

Other

  • test: Add Options Documentation Sync Tests by philipphofmann in #7075

This preview updates automatically when you update the PR.

[Go]
Comment thread CHANGELOG.md Resolved
- Updated `SentryFramesTracker` to ensure listeners are managed on the main queue, preventing potential crashes.
- Made `isStarted` property private for better encapsulation.
- Enhanced deinitialization to safely stop the tracker and avoid async dispatch issues.

denrase commented Jan 13, 2026

Copy link
Copy Markdown
Collaborator Author

@philprime Would be great if you could take another look. I tried reproducing again after the cursor bot comment and could get the frames tracker to crash using this code in the sample app. I added additional precautions to fix this, which did work. While I still could not reproduce the initial issue, this should offer more protection against potential races.

    @IBAction func useAfterFree(_ sender: UIButton) {
        highlightButton(sender)
        
        let framesTracker = SentryDependencyContainer.sharedInstance().framesTracker
        
        class TestListener: NSObject, SentryFramesTrackerListener {
            func framesTrackerHasNewFrame(_ newFrameDate: Date) {
                // Access during callback to create more contention
            }
        }
        
        var listeners: [TestListener] = []
        for _ in 0..<50 {
            let listener = TestListener()
            listeners.append(listener)
            framesTracker.addListener(listener)
        }
        for _ in 0..<20 {
            DispatchQueue.global().async {
                for listener in listeners {
                    for i in 0..<100 {
                        framesTracker.removeListener(listener)
                        if i % 2 == 0 {
                            framesTracker.stop()
                        } else {
                            framesTracker.start()
                        }
                    }
                }
            }
        }
    }
Bildschirmfoto 2026-01-13 um 11 53 31 [Bildschirmfoto 2026-01-13 um 11 53 31]
[Go]

@philprime [@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.

[Hide comment]

LGTM, this also seems to be more consistent. Maybe it makes sense if @itaybre or @philipphofmann also gives this a second review

[Go]
@philippsuess [@philippsuess] philippsuess changed the title Resolve crash in caused by calling SentryFramesTracker.removeListener(_:) fix: Crash in SentryFramesTracker.removeListener(_:) Jan 13, 2026
@philippsuess [@philippsuess] philippsuess changed the title fix: Crash in SentryFramesTracker.removeListener(_:) fix: Crash in SentryFramesTracker.removeListener Jan 13, 2026

@philippsuess [@philippsuess] philippsuess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Hide comment]

Honestly, I'm unsure about the dispatchSyncOnMainQueue in deinit.

[Go]

@philippsuess [@philippsuess] philippsuess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Hide comment]

LGTM, thanks

[Go]
Hide details View details
denrase merged commit f100534 into main Jan 20, 2026
200 checks passed
denrase deleted the fix/frames-tracker-remove-listner-crash branch January 20, 2026 15:38
itaybre added a commit that referenced this pull request Apr 28, 2026
* fix: race in SentryFramesTracker listeners

Replace NSHashTable.weakObjects() with a Swift array of
weakly-boxed listeners. NSHashTable's internal bucket slots
can be zeroed by the ObjC runtime on the thread that
deallocates a stored listener while another thread is
iterating the table, causing NSConcreteHashTable removeItem:
to objc_msgSend a freed entry.

Ensuring all mutations run on the main thread (fixed in
#7155) is not sufficient: listener objects can still be
deallocated on any thread, and the weak-zeroing races with
the table's iteration. Reading our own weak-boxed listeners
uses objc_loadWeak and is safe against concurrent
deallocation.

* fix: add changelog entry for SentryFramesTracker listener race (#7839)

* Use WeakReference

* Fix tests

* Simplify listenersCount calculation in SentryFramesTracker
itaybre added a commit that referenced this pull request May 29, 2026
* fix: FramesTracker listener-capture deinit race

Both `addListener` and `removeListener` strongly captured `listener` in
the closure passed to `dispatchAsyncOnMainQueueIfNotMainThread`. When a
caller invoked either method from inside its own `init` / `deinit` on a
background thread, the dispatched main-queue block tried to retain a
freed (or mid-dealloc) object once it reached main. Under iOS 26's
hardened ARC this is fatal: `swift_unknownObjectRetain`  `EXC_BREAKPOINT`.
The `removeListener`-from-`deinit` shape is the documented motivating
crash (lldb-confirmed in a Capacitor + WKWebView host); the `addListener`
mirror is theoretical but the fix is symmetric and cheap.

Capture `ObjectIdentifier(listener)` (a value type) and a pre-built
`WeakReference(value: listener)` instead, so the closures never hold
the listener directly. The main-queue hop no longer extends the
listener's lifetime, and identity matching against still-alive entries
(read via `WeakReference.value` / `objc_loadWeak`) keeps working. Adds
`[weak self]` to match the lifetime-safety theme of the rest of the file
(precedent: `SentryUIDeviceWrapper.start`).

This is the third crash class in the same method area. #7155 fixed the
original Obj-C `removeListener` crash; #7839 fixed the
`NSConcreteHashTable` race that the comment on the `listeners` property
records. The remaining race lived in the Swift closure-capture path,
exposed when iOS 26 hardened ARC's retain-on-mid-dealloc check.

Adds `testAddAndRemoveListener_DoNotRetainListenerAcrossMainQueueHop`,
a deterministic regression test using `TestSentryDispatchQueueWrapper`
to defer block execution. The test verifies the structural invariant
("closure must not strongly retain the listener") via a `weak`
reference assertion, which fails loudly on the pre-fix code with a
clear error message:

    XCTAssertNil failed: "<FrameTrackerListener: 0x...>" -
    Closures dispatched by addListener/removeListener must not retain
    the listener across the main-queue hop

* Update CHANGELOG.md

---------

Co-authored-by: Itay Brenner <itaybrenner@hotmail.com>
Co-authored-by: Itay Brenner <itay.brenner@sentry.io>
itaybre added a commit that referenced this pull request Jun 1, 2026
* fix: FramesTracker listener-capture deinit race

Both `addListener` and `removeListener` strongly captured `listener` in
the closure passed to `dispatchAsyncOnMainQueueIfNotMainThread`. When a
caller invoked either method from inside its own `init` / `deinit` on a
background thread, the dispatched main-queue block tried to retain a
freed (or mid-dealloc) object once it reached main. Under iOS 26's
hardened ARC this is fatal: `swift_unknownObjectRetain`  `EXC_BREAKPOINT`.
The `removeListener`-from-`deinit` shape is the documented motivating
crash (lldb-confirmed in a Capacitor + WKWebView host); the `addListener`
mirror is theoretical but the fix is symmetric and cheap.

Capture `ObjectIdentifier(listener)` (a value type) and a pre-built
`WeakReference(value: listener)` instead, so the closures never hold
the listener directly. The main-queue hop no longer extends the
listener's lifetime, and identity matching against still-alive entries
(read via `WeakReference.value` / `objc_loadWeak`) keeps working. Adds
`[weak self]` to match the lifetime-safety theme of the rest of the file
(precedent: `SentryUIDeviceWrapper.start`).

This is the third crash class in the same method area. #7155 fixed the
original Obj-C `removeListener` crash; #7839 fixed the
`NSConcreteHashTable` race that the comment on the `listeners` property
records. The remaining race lived in the Swift closure-capture path,
exposed when iOS 26 hardened ARC's retain-on-mid-dealloc check.

Adds `testAddAndRemoveListener_DoNotRetainListenerAcrossMainQueueHop`,
a deterministic regression test using `TestSentryDispatchQueueWrapper`
to defer block execution. The test verifies the structural invariant
("closure must not strongly retain the listener") via a `weak`
reference assertion, which fails loudly on the pre-fix code with a
clear error message:

    XCTAssertNil failed: "<FrameTrackerListener: 0x...>" -
    Closures dispatched by addListener/removeListener must not retain
    the listener across the main-queue hop

* Update CHANGELOG.md

---------

Co-authored-by: Itay Brenner <itaybrenner@hotmail.com>
Co-authored-by: Itay Brenner <itay.brenner@sentry.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge (deprecated) Use run-full-ci instead.

Footer

© 2026 GitHub, Inc.

Web Proxy Viewer  |  New URL  |  Original Page