Skip to content

fix(auth): release removed state and ID token listeners - #16962

Merged
paulb777 merged 4 commits into
mainfrom
pb-auth-listener-leak
Oct 27, 2026
Merged

paulb777 merged 4 commits into
mainfrom
pb-auth-listener-leak

Conversation

@paulb777

@paulb777 paulb777 commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Summary

Store the observer handle returned by NotificationCenter in
listenerHandles, instead of the listener closure. Removing an auth state or
ID token listener now releases it, and deinit removes the observers that are
still registered.

Fixes #16938.

Why

addIDTokenDidChangeListener(_:) added the listener closure to
listenerHandles, but removeIDTokenDidChangeListener(_:),
removeStateDidChangeListener(_:), and deinit expect the handles returned by
NotificationCenter.addObserver(forName:object:queue:using:) there. A handle
never matched a closure, so:

  • Removing a listener left its closure in listenerHandles. The listener, and
    everything it captured, stayed in memory for the lifetime of Auth, which
    for Auth.auth() is the life of the app. Apps that add and remove listeners
    repeatedly, for example one per screen or per subscription, accumulated them
    without bound.
  • deinit passed the closures to removeObserver(_:), which did nothing, so
    the observers of a deallocated Auth stayed registered with
    NotificationCenter, along with their listeners.

addStateDidChangeListener(_:) is built on addIDTokenDidChangeListener(_:),
and the authStateChanges and idTokenChanges sequences and the Combine
publishers use these methods, so all of them were affected. The Objective-C SDK
stored the handle. This regressed in the Swift port (#11237), and has been
present since 11.0.0.

Changes

  • addIDTokenDidChangeListener(_:) adds the observer handle to
    listenerHandles, like the Objective-C SDK did.
  • listenerHandles is now [NSObjectProtocol] instead of NSMutableArray, so
    the compiler rejects adding anything other than an observer handle, such as
    the closure. The remove methods remove the handle by identity, and deinit
    removes each remaining observer from NotificationCenter.
  • deinit also removes the didBecomeActiveNotification and
    didEnterBackgroundNotification observers on tvOS, where
    getToken(forcingRefresh:completion:) adds them too. Before, it only removed
    them on iOS (from review).
  • CHANGELOG entry.

Notes for reviewers

  • Matching: NSMutableArray.remove(_:) matched with isEqual:. The
    handles are __NSObserver instances, which don't override isEqual:, so
    matching by identity (===) finds the same handles.
  • Locking: add and remove still change the array while holding the
    Auth.self lock, and now release it with defer, like
    removeStateDidChangeListener(_:) (from review). In
    addIDTokenDidChangeListener(_:), the defer is in a do block, so the lock
    isn't held while the initial call is dispatched. deinit doesn't take the
    lock, as before, since it only runs when nothing else holds the instance.
  • Initial call: a listener that is removed before its initial call runs is
    still called once, and is released after that. The Objective-C SDK behaved
    the same way. Not changed here.

Testing

  • New tests in AuthTests, which check through a weak reference that an object
    captured by a listener is deallocated:
    • testRemoveStateDidChangeListenerReleasesListener and
      testRemoveIDTokenDidChangeListenerReleasesListener add a listener, remove
      it, and let the initial call run.
    • testDeallocatingAuthRemovesListeners adds both kinds of listener to a new
      Auth, and lets it deallocate without removing them. It also checks that
      the Auth instance is deallocated.
  • Check that the tests catch the bug: on main, all three fail. In
    testDeallocatingAuthRemovesListeners, the Auth instance is deallocated,
    but the captured object isn't, because NotificationCenter still holds the
    observer.
  • xcodebuild -scheme AuthUnit -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.5' test: 403 tests, 0 failures.
  • xcodebuild -scheme AuthUnit -destination 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation),OS=26.5' test: 296 tests, 0 failures.
  • The three new tests passed 200 times in a row (-test-iterations 200).

Related

Noted under "Not included" in #16856.

Store the observer handle returned by `NotificationCenter` in
`listenerHandles`, instead of the listener closure. The remove methods and
`deinit` look for the handle there, so they never found the closure.
Previously, removed listeners, and the objects they captured, stayed in
memory for the lifetime of `Auth`, and `deinit` left the observers
registered with `NotificationCenter`. The Objective-C SDK stored the handle;
this regressed in the Swift port in 11.0.0.

Type `listenerHandles` as `[NSObjectProtocol]`, so the compiler rejects
storing anything other than an observer handle.

Fixes #16938
@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

@paulb777

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request resolves a memory leak in FirebaseAuth where removed auth state and ID token listeners, along with their captured objects, were retained in memory. The fix transitions listenerHandles from NSMutableArray to a Swift array of [NSObjectProtocol], simplifying the addition, removal, and deinitialization cleanup. Additionally, new unit tests are introduced to verify that listeners are correctly released. The review feedback suggests extending the platform check in deinit to include tvOS to prevent observer leaks on that platform as well.

Comment thread FirebaseAuth/Sources/Swift/Auth/Auth.swift Outdated
`getToken(forcingRefresh:completion:)` adds the `didBecomeActive` and
`didEnterBackground` observers on iOS and tvOS, but `deinit` only removed
them on iOS. Remove them on tvOS as well, using the same platform condition.
@paulb777

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request resolves a memory leak in FirebaseAuth by migrating listenerHandles from NSMutableArray to a native Swift array of [NSObjectProtocol]. This allows proper cleanup of auth state and ID token listeners, ensuring they and their captured objects are released when removed or when the Auth instance is deallocated. Additionally, new unit tests have been added to verify this behavior. The review feedback suggests improving safety and consistency by using defer { objc_sync_exit(Auth.self) } immediately after entering synchronized blocks in addIDTokenDidChangeListener and removeIDTokenDidChangeListener.

Comment thread FirebaseAuth/Sources/Swift/Auth/Auth.swift Outdated
Comment thread FirebaseAuth/Sources/Swift/Auth/Auth.swift Outdated
Release the `Auth.self` lock with `defer` in
`addIDTokenDidChangeListener(_:)` and `removeIDTokenDidChangeListener(_:)`,
like `removeStateDidChangeListener(_:)`. In
`addIDTokenDidChangeListener(_:)`, put the `defer` in a `do` block, so the
lock is released right after the handle is added, not at the end of the
method.
@paulb777 paulb777 assigned ncooke3 and macastelaz and unassigned ncooke3 Oct 27, 2026
@paulb777
paulb777 merged commit 4bc5060 into main Oct 27, 2026
94 checks passed
@paulb777
paulb777 deleted the pb-auth-listener-leak branch October 27, 2026 15:43
@paulb777 paulb777 added this to the 13.2.0 - M189 milestone Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auth: removed state/ID token listeners stay retained for the lifetime of Auth

3 participants