Skip to content

[iOS] Fix missing nil checks and improve SemanticsObject bridge API#189630

Merged
cbracken merged 1 commit into
flutter:masterfrom
cbracken:fix-a11y-bridge-weakptr-api
Jul 17, 2026
Merged

[iOS] Fix missing nil checks and improve SemanticsObject bridge API#189630
cbracken merged 1 commit into
flutter:masterfrom
cbracken:fix-a11y-bridge-weakptr-api

Conversation

@cbracken

@cbracken cbracken commented Jul 16, 2026

Copy link
Copy Markdown
Member

UIKit and VoiceOver can retain SemanticsObject/SemanticsObjectContainer instances after the engine's AccessibilityBridge that created them has been destroyed, e.g. during engine shutdown, view controller teardown, or a FlutterViewController being swapped out while VoiceOver still holds a reference to its accessibility tree (e.g. #43795). Existing code already guarded most UIAccessibilityElement/UIAccessibilityContainer overrides for this with an isAccessibilityBridgeAlive check followed by a separate self.bridge re-read, but a few call sites had no guard at all.

In debug builds, fml::WeakPtr's debug-mode assertion will trigger an abort() in such cases, but this is undefined behaviour in release builds.

This patch replaces the fml::WeakPtr<AccessibilityBridgeIos> bridge property on SemanticsObject with - (AccessibilityBridgeIos*)bridge and - (UIView*)bridgeView accessor methods.

bridge resolves the weak pointer and hands back a raw AccessibilityBridgeIos* or nullptr. Callers fetch it once into a local, check it against nullptr, and use it synchronously within that same scope. This simplifies the previous two-step pattern (an isAccessibilityBridgeAlive check followed by a separate self.bridge re-read, which could in theory observe different liveness between the two) into one step, and keeps the C++ smart pointer type out of the Objective-C API. Similar to the previous code (when we did the two-step process to get the raw pointer at each call site), these pointers should never escape the scope in which they're created.

All overrides that dereferenced the bridge have been updated to the new API, including the ones that previously had no check at all:

  • showOnScreen
  • onCustomAccessibilityAction:
  • ConvertPointToGlobal
  • ConvertRectToGlobal
  • _accessibilityHitTest:withEvent: (which VoiceOver calls directly during touch exploration),
  • the TextInputSemanticsObject overrides

bridgeView hands back the ARC-managed UIView* itself. Once a caller holds a strong reference to it, the view is memory-safe independent of the bridge's C++ lifetime. That view can still outlive the bridge, (it's owned separately by the view hierarchy), so it's longer driven by the engine once the bridge is gone. Both accessors return nil once the bridge is dead.

This allows us to delete the redundant _bridge ivar from SemanticsObjectContainer in favor of deriving bridgeView as needed from its semanticsObject, so there's a single source of truth for bridge liveness.

I've also wrapped SemanticsObject.h in NS_ASSUME_NONNULL_BEGIN/END now that bridge/bridgeView are nullable, and made parent and SemanticsObjectContainer.semanticsObject weak accordingly.

Added a test that destroys an AccessibilityBridge then confirms that SemanticsObject/ SemanticsObjectContainer accessors, actions, geometry conversion, and hit-testing no longer touch freed memory afterward.

Issue: b/525528671

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@cbracken
cbracken requested a review from hellohuanlin July 16, 2026 23:54
@cbracken
cbracken requested a review from a team as a code owner July 16, 2026 23:54
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Jul 16, 2026
@github-actions github-actions Bot added a: text input Entering text in a text field or keyboard related problems platform-ios iOS applications specifically engine flutter/engine related. See also e: labels. a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) team-ios Owned by iOS platform team labels Jul 16, 2026

@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 refactors the iOS accessibility bridge interaction in SemanticsObject and related classes to safely handle cases where the accessibility bridge has been destroyed, and adds nullability annotations along with a unit test. The review feedback recommends returning early with zero values in coordinate conversion functions if the bridge view is nil, reverting the _children array type change to NSArray to avoid unsafe casts and potential runtime crashes, simplifying scale calculations using the new bridgeView helper, and declaring the internal weakBridge as a private ivar instead of a public property.

Comment thread engine/src/flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm Outdated
Comment thread engine/src/flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm Outdated
Comment thread engine/src/flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm Outdated
Comment thread engine/src/flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm Outdated
Comment thread engine/src/flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm Outdated
Comment thread engine/src/flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm Outdated
@cbracken
cbracken force-pushed the fix-a11y-bridge-weakptr-api branch 2 times, most recently from 8da37b2 to 3dc0de0 Compare July 17, 2026 00:14
UIKit and VoiceOver can retain `SemanticsObject`/`SemanticsObjectContainer`
instances after the engine's `AccessibilityBridge` that created them has
been destroyed, e.g. during engine shutdown, view controller teardown, or
a `FlutterViewController` being swapped out while VoiceOver still holds a
reference to its accessibility tree (e.g. flutter#43795). Existing
code already guarded *most* `UIAccessibilityElement`/`UIAccessibilityContainer`
overrides for this with an `isAccessibilityBridgeAlive` check followed by a
separate `self.bridge` re-read, but a few call sites had no guard at all.

In debug builds, `fml::WeakPtr`'s debug-mode assertion will trigger an
`abort()` in such cases, but this is undefined behaviour in release
builds.

This patch replaces the `fml::WeakPtr<AccessibilityBridgeIos> bridge`
property on `SemanticsObject` with `- (AccessibilityBridgeIos*)bridge` and
`- (UIView*)bridgeView` accessor methods.

`bridge` resolves the weak pointer and hands back a raw
`AccessibilityBridgeIos*` or nullptr. Callers fetch it once into a
local, check it against nullptr, and use it synchronously within that
same scope. This simplifies the previous two-step pattern (an
`isAccessibilityBridgeAlive` check followed by a separate `self.bridge`
re-read, which could in theory observe different liveness between the
two) into one step, and keeps the C++ smart pointer type out of the
Objective-C API. Similar to the previous code (when we did the two-step
process to get the raw pointer at each call site), these pointers should
never escape the scope in which they're created.

All overrides that dereferenced the bridge have been updated to the new
API, including the ones that previously had no check at all:

  * `showOnScreen`
  * `onCustomAccessibilityAction:`
  * `ConvertPointToGlobal`
  * `ConvertRectToGlobal`
  * `_accessibilityHitTest:withEvent:` (which VoiceOver calls directly during touch exploration),
  * the `TextInputSemanticsObject` overrides

`ConvertPointToGlobal`/`ConvertRectToGlobal` no longer lean on an
`FML_DCHECK` to catch a nil bridge -- since that's a debug-only assertion,
a release build would have fallen through to running the rest of the
coordinate-conversion math (and ultimately messaging nil) instead of
actually being protected. Both now return `CGPointZero`/`CGRectZero`
immediately if `bridgeView` is nil, regardless of build configuration.

`bridgeView` hands back the ARC-managed `UIView*` itself. Once a caller
holds a strong reference to it, the view is memory-safe independent of
the bridge's C++ lifetime. That view can still outlive the bridge, (it's
owned separately by the view hierarchy), so it's no longer driven by the
engine once the bridge is gone. Both accessors return nil once the
bridge is dead.

This allows us to delete the redundant `_bridge` ivar from
`SemanticsObjectContainer` in favor of deriving `bridgeView` as needed
from its `semanticsObject`, so there's a single source of truth for
bridge liveness.

I've also wrapped `SemanticsObject.h` in `NS_ASSUME_NONNULL_BEGIN`/`END` now
that `bridge`/`bridgeView` are nullable, and made `parent` and
`SemanticsObjectContainer.semanticsObject` `weak` accordingly.

`_children` was declared `NSMutableArray<SemanticsObject*>*` but the
property was declared `NSArray`. Adding nullability annotations caused
errors about this; I've implemented the getter manually rather than
relying on property synthesis. We probably should have done this in the
first place given the differing types in the existing code.

Also simplifies the scale calculation in the `frame` override in
`SemanticsObject+UIFocusSystem.mm` to use `bridgeView` instead of
manually resolving `bridge`. This also fixes a potential divide-by-zero,
since the old ternary never fell back to `UIScreen.mainScreen` when the
bridge was alive but `.screen` was nil.

Added a test that destroys an `AccessibilityBridge` then confirms that
`SemanticsObject`/`SemanticsObjectContainer` accessors, actions,
geometry conversion, and hit-testing no longer touch freed memory
afterward.
@cbracken
cbracken force-pushed the fix-a11y-bridge-weakptr-api branch from 3dc0de0 to af233d4 Compare July 17, 2026 00:27

@hellohuanlin hellohuanlin 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.

LGTM! Thanks for walking over this.

@cbracken
cbracken enabled auto-merge July 17, 2026 01:13
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
`AccessibilityBridge`'s constructor registers a message handler Obj-C
block on `accessibility_channel_` (the "flutter/accessibility" channel)
that calls the class's `HandleEvent(...)` method directly:

    [accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
      HandleEvent((NSDictionary*)message);
    }];

This implicitly captures `this` as a raw C++ pointer. ARC manages the
block and any captured Obj-C objects, but has no idea about the raw C++
`this` pointer and we do nothing to keep the `AccessibilityBridge` alive
or to null out `this` when it's destroyed. If a message arrives on this
channel while or after the bridge is being torn down (e.g. during engine
shutdown), we'll dereference the (now garbage) dangling `AccessibilityBridge*`.

We fix this by capturing `fml::WeakPtr<AccessibilityBridge> weak_self =
GetWeakPtr()` by value in the block instead, and checking it before calling
`HandleEvent`. The `weak_factory_` is already declared last in the
`AccessibilityBridge` class member list, so it'll be invalidated first
in the class destructor, so we're not at risk of using the bridge in a
half torn-down state. Messages arriving anytime after the start of the
destructor are now handled as a no-op.

I've added a test which captures the message handler registered on the
channel, destroys the `AccessibilityBridge`, and invokes the captured
handler to confirm it no longer touches dealloc'ed memory.

Issue: b/525528671
Related: flutter#189630
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
`AccessibilityBridge`'s constructor registers a message handler Obj-C
block on `accessibility_channel_` (the "flutter/accessibility" channel)
that calls the class's `HandleEvent(...)` method directly:

    [accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
      HandleEvent((NSDictionary*)message);
    }];

This implicitly captures `this` as a raw C++ pointer. ARC manages the
block and any captured Obj-C objects, but has no idea about the raw C++
`this` pointer and we do nothing to keep the `AccessibilityBridge` alive
or to null out `this` when it's destroyed. If a message arrives on this
channel while or after the bridge is being torn down (e.g. during engine
shutdown), we'll dereference the (now garbage) dangling `AccessibilityBridge*`.

We fix this by capturing `fml::WeakPtr<AccessibilityBridge> weak_self =
GetWeakPtr()` by value in the block instead, and checking it before calling
`HandleEvent`. The `weak_factory_` is already declared last in the
`AccessibilityBridge` class member list, so it'll be invalidated first
in the class destructor, so we're not at risk of using the bridge in a
half torn-down state. Messages arriving anytime after the start of the
destructor are now handled as a no-op.

I've added a test which captures the message handler registered on the
channel, destroys the `AccessibilityBridge`, and invokes the captured
handler to confirm it no longer touches dealloc'ed memory.

Issue: b/525528671
Related: flutter#189630
@cbracken cbracken changed the title [iOS] Improve SemanticsObject's accessibility bridge accessors [iOS] Fix missing nil checks and improve SemanticsObject bridge accessors Jul 17, 2026
@cbracken cbracken changed the title [iOS] Fix missing nil checks and improve SemanticsObject bridge accessors [iOS] Fix missing nil checks and improve SemanticsObject bridge API Jul 17, 2026
@cbracken
cbracken added this pull request to the merge queue Jul 17, 2026
Merged via the queue into flutter:master with commit 7ea98d8 Jul 17, 2026
17 checks passed
@cbracken
cbracken deleted the fix-a11y-bridge-weakptr-api branch July 17, 2026 05:26
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
`AccessibilityBridge`'s constructor registers a message handler Obj-C
block on `accessibility_channel_` (the "flutter/accessibility" channel)
that calls the class's `HandleEvent(...)` method directly:

    [accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
      HandleEvent((NSDictionary*)message);
    }];

This implicitly captures `this` as a raw C++ pointer. ARC manages the
block and any captured Obj-C objects, but has no idea about the raw C++
`this` pointer and we do nothing to keep the `AccessibilityBridge` alive
or to null out `this` when it's destroyed. If a message arrives on this
channel while or after the bridge is being torn down (e.g. during engine
shutdown), we'll dereference the (now garbage) dangling `AccessibilityBridge*`.

We fix this by capturing `fml::WeakPtr<AccessibilityBridge> weak_self =
GetWeakPtr()` by value in the block instead, and checking it before calling
`HandleEvent`. The `weak_factory_` is already declared last in the
`AccessibilityBridge` class member list, so it'll be invalidated first
in the class destructor, so we're not at risk of using the bridge in a
half torn-down state. Messages arriving anytime after the start of the
destructor are now handled as a no-op.

I've added a test which captures the message handler registered on the
channel, destroys the `AccessibilityBridge`, and invokes the captured
handler to confirm it no longer touches dealloc'ed memory.

Issue: b/525528671
Related: flutter#189630
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
`AccessibilityBridge`'s constructor registers a message handler Obj-C
block on `accessibility_channel_` (the "flutter/accessibility" channel)
that calls the class's `HandleEvent(...)` method directly:

    [accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
      HandleEvent((NSDictionary*)message);
    }];

This implicitly captures `this` as a raw C++ pointer. ARC manages the
block and any captured Obj-C objects, but has no idea about the raw C++
`this` pointer and we do nothing to keep the `AccessibilityBridge` alive
or to null out `this` when it's destroyed. If a message arrives on this
channel while or after the bridge is being torn down (e.g. during engine
shutdown), we'll dereference the (now garbage) dangling `AccessibilityBridge*`.

We fix this by capturing `fml::WeakPtr<AccessibilityBridge> weak_self =
GetWeakPtr()` by value in the block instead, and checking it before calling
`HandleEvent`. The `weak_factory_` is already declared last in the
`AccessibilityBridge` class member list, so it'll be invalidated first
in the class destructor, so we're not at risk of using the bridge in a
half torn-down state. Messages arriving anytime after the start of the
destructor are now handled as a no-op.

I've added a test which captures the message handler registered on the
channel, destroys the `AccessibilityBridge`, and invokes the captured
handler to confirm it no longer touches dealloc'ed memory.

Issue: b/525528671
Related: flutter#189630
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
`AccessibilityBridge`'s constructor registers a message handler Obj-C
block on `accessibility_channel_` (the "flutter/accessibility" channel)
that calls the class's `HandleEvent(...)` method directly:

    [accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
      HandleEvent((NSDictionary*)message);
    }];

This implicitly captures `this` as a raw C++ pointer. ARC manages the
block and any captured Obj-C objects, but has no idea about the raw C++
`this` pointer and we do nothing to keep the `AccessibilityBridge` alive
or to null out `this` when it's destroyed. If a message arrives on this
channel while or after the bridge is being torn down (e.g. during engine
shutdown), we'll dereference the (now garbage) dangling `AccessibilityBridge*`.

We fix this by capturing `fml::WeakPtr<AccessibilityBridge> weak_self =
GetWeakPtr()` by value in the block instead, and checking it before calling
`HandleEvent`. The `weak_factory_` is already declared last in the
`AccessibilityBridge` class member list, so it'll be invalidated first
in the class destructor, so we're not at risk of using the bridge in a
half torn-down state. Messages arriving anytime after the start of the
destructor are now handled as a no-op.

I've added a test which captures the message handler registered on the
channel, destroys the `AccessibilityBridge`, and invokes the captured
handler to confirm it no longer touches dealloc'ed memory.

Issue: b/525528671
Related: flutter#189630
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
`AccessibilityBridge`'s constructor registers a message handler Obj-C
block on `accessibility_channel_` (the "flutter/accessibility" channel)
that calls the class's `HandleEvent(...)` method directly:

    [accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
      HandleEvent((NSDictionary*)message);
    }];

This implicitly captures `this` as a raw C++ pointer. ARC manages the
block and any captured Obj-C objects, but has no idea about the raw C++
`this` pointer and we do nothing to keep the `AccessibilityBridge` alive
or to null out `this` when it's destroyed. If a message arrives on this
channel while or after the bridge is being torn down (e.g. during engine
shutdown), we'll dereference the (now garbage) dangling `AccessibilityBridge*`.

We fix this by capturing `fml::WeakPtr<AccessibilityBridge> weak_self =
GetWeakPtr()` by value in the block instead, and checking it before calling
`HandleEvent`. The `weak_factory_` is already declared last in the
`AccessibilityBridge` class member list, so it'll be invalidated first
in the class destructor, so we're not at risk of using the bridge in a
half torn-down state. Messages arriving anytime after the start of the
destructor are now handled as a no-op.

I've added a test which captures the message handler registered on the
channel, destroys the `AccessibilityBridge`, and invokes the captured
handler to confirm it no longer touches dealloc'ed memory.

Issue: b/525528671
Related: flutter#189630
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
`AccessibilityBridge`'s constructor registers a message handler Obj-C
block on `accessibility_channel_` (the "flutter/accessibility" channel)
that calls the class's `HandleEvent(...)` method directly:

    [accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
      HandleEvent((NSDictionary*)message);
    }];

This implicitly captures `this` as a raw C++ pointer. ARC manages the
block and any captured Obj-C objects, but has no idea about the raw C++
`this` pointer and we do nothing to keep the `AccessibilityBridge` alive
or to null out `this` when it's destroyed. If a message arrives on this
channel while or after the bridge is being torn down (e.g. during engine
shutdown), we'll dereference the (now garbage) dangling `AccessibilityBridge*`.

We fix this by capturing `fml::WeakPtr<AccessibilityBridge> weak_self =
GetWeakPtr()` by value in the block instead, and checking it before calling
`HandleEvent`. The `weak_factory_` is already declared last in the
`AccessibilityBridge` class member list, so it'll be invalidated first
in the class destructor, so we're not at risk of using the bridge in a
half torn-down state. Messages arriving anytime after the start of the
destructor are now handled as a no-op.

I've added a test which captures the message handler registered on the
channel, destroys the `AccessibilityBridge`, and invokes the captured
handler to confirm it no longer touches dealloc'ed memory.

Issue: b/525528671
Related: flutter#189630
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
`AccessibilityBridge`'s constructor registers a message handler Obj-C
block on `accessibility_channel_` (the "flutter/accessibility" channel)
that calls the class's `HandleEvent(...)` method directly:

    [accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
      HandleEvent((NSDictionary*)message);
    }];

This implicitly captures `this` as a raw C++ pointer. ARC manages the
block and any captured Obj-C objects, but has no idea about the raw C++
`this` pointer and we do nothing to keep the `AccessibilityBridge` alive
or to null out `this` when it's destroyed. If a message arrives on this
channel while or after the bridge is being torn down (e.g. during engine
shutdown), we'll dereference the (now garbage) dangling `AccessibilityBridge*`.

We fix this by capturing `fml::WeakPtr<AccessibilityBridge> weak_self =
GetWeakPtr()` by value in the block instead, and checking it before calling
`HandleEvent`. The `weak_factory_` is already declared last in the
`AccessibilityBridge` class member list, so it'll be invalidated first
in the class destructor, so we're not at risk of using the bridge in a
half torn-down state. Messages arriving anytime after the start of the
destructor are now handled as a no-op.

I've added a test which captures the message handler registered on the
channel, destroys the `AccessibilityBridge`, and invokes the captured
handler to confirm it no longer touches dealloc'ed memory.

Issue: b/525528671
Related: flutter#189630
cbracken added a commit to cbracken/flutter that referenced this pull request Jul 17, 2026
`AccessibilityBridge`'s constructor registers a message handler Obj-C
block on `accessibility_channel_` (the "flutter/accessibility" channel)
that calls the class's `HandleEvent(...)` method directly:

    [accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
      HandleEvent((NSDictionary*)message);
    }];

This implicitly captures `this` as a raw C++ pointer. ARC manages the
block and any captured Obj-C objects, but has no idea about the raw C++
`this` pointer and we do nothing to keep the `AccessibilityBridge` alive
or to null out `this` when it's destroyed. If a message arrives on this
channel while or after the bridge is being torn down (e.g. during engine
shutdown), we'll dereference the (now garbage) dangling `AccessibilityBridge*`.

We fix this by capturing `fml::WeakPtr<AccessibilityBridge> weak_self =
GetWeakPtr()` by value in the block instead, and checking it before calling
`HandleEvent`. The `weak_factory_` is already declared last in the
`AccessibilityBridge` class member list, so it'll be invalidated first
in the class destructor, so we're not at risk of using the bridge in a
half torn-down state. Messages arriving anytime after the start of the
destructor are now handled as a no-op.

I've added a test which captures the message handler registered on the
channel, destroys the `AccessibilityBridge`, and invokes the captured
handler to confirm it no longer touches dealloc'ed memory.

Issue: b/525528671
Related: flutter#189630
chunfengyao pushed a commit to chunfengyao/flutter that referenced this pull request Jul 17, 2026
…utter#189637)

`AccessibilityBridge`'s constructor registers a message handler Obj-C
block on `accessibility_channel_` (the "flutter/accessibility" channel)
that calls the class's `HandleEvent(...)` method directly:

[accessibility_channel_ setMessageHandler:^(id message, FlutterReply
reply) {
      HandleEvent((NSDictionary*)message);  // Implicit this capture.
    }];

This implicitly captures `this` as a raw C++ pointer. ARC manages the
block and any captured Obj-C objects, but has no idea about the raw C++
`this` pointer and we do nothing to keep the `AccessibilityBridge` alive
or to null out `this` when it's destroyed. If a message arrives on this
channel while or after the bridge is being torn down (e.g. during engine
shutdown), we'll dereference the (now garbage) dangling
`AccessibilityBridge*`.

We fix this by capturing `fml::WeakPtr<AccessibilityBridge> weak_self =
GetWeakPtr()` by value in the block instead, and checking it before
calling `HandleEvent`. The `weak_factory_` is already declared last in
the `AccessibilityBridge` class member list, so it'll be invalidated
first in the class destructor, so we're not at risk of using the bridge
in a half torn-down state. Messages arriving anytime after the start of
the destructor are now handled as a no-op.

I've added a test which captures the message handler registered on the
channel, destroys the `AccessibilityBridge`, and invokes the captured
handler to confirm it no longer touches dealloc'ed memory.

Issue: b/525528671
Related: flutter#189630

<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->


## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

If this change needs to override an active code freeze, provide a
comment explaining why. The code freeze workflow can be overridden by
code reviewers. See pinned issues for any active code freezes with
guidance.

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Jul 18, 2026
Roll Flutter from fc1ad955f164 to 8005793c3562 (40 revisions)

flutter/flutter@fc1ad95...8005793

2026-07-18 [email protected] Remove `LineContents ` experimental AA line shader that is no longer used (flutter/flutter#189619)
2026-07-18 [email protected] Roll Skia from 9468e96cc40f to ba90f98535de (8 revisions) (flutter/flutter#189680)
2026-07-17 [email protected] Roll Dart SDK from 33e4b71e984b to 666e1e2133b7 (2 revisions) (flutter/flutter#189673)
2026-07-17 [email protected] [iOS] Fix potential use-after-free in a11y bridge channel handler (flutter/flutter#189637)
2026-07-17 [email protected] Migrate dev/tools skill validation to new dart_skills_lint API (flutter/flutter#189626)
2026-07-17 [email protected] Add validation for required fields during xcodebuild (flutter/flutter#187772)
2026-07-17 [email protected] Upgrade android_hardware_smoke_test CI to run instrumented tests (flutter/flutter#189390)
2026-07-17 [email protected] Roll Dart SDK from 0867cb1897b5 to 33e4b71e984b (1 revision) (flutter/flutter#189661)
2026-07-17 [email protected] Roll Skia from 702c3e790232 to 9468e96cc40f (1 revision) (flutter/flutter#189660)
2026-07-17 [email protected] Roll Packages from 9f95026 to 4fdc766 (11 revisions) (flutter/flutter#189659)
2026-07-17 [email protected] Roll Skia from 2e4a3ae035cd to 702c3e790232 (1 revision) (flutter/flutter#189655)
2026-07-17 [email protected] [macOS] Add FlutterPluginRegistrar.valuePublished(byPlugin:) just like iOS (flutter/flutter#189614)
2026-07-17 [email protected] Roll Dart SDK from c69d138c9646 to 0867cb1897b5 (2 revisions) (flutter/flutter#189649)
2026-07-17 [email protected] Roll Skia from 4f5aca109c87 to 2e4a3ae035cd (3 revisions) (flutter/flutter#189646)
2026-07-17 [email protected] [iOS] Fix missing nil checks and improve SemanticsObject bridge API (flutter/flutter#189630)
2026-07-17 [email protected] Roll Skia from 37c5e6b26aee to 4f5aca109c87 (19 revisions) (flutter/flutter#189638)
2026-07-17 [email protected] Roll Fuchsia Linux SDK from lLFbh5kFWbUGgC9Ek... to NL8xtzr8cxr5E8r8E... (flutter/flutter#189632)
2026-07-17 [email protected] Add blendMode parameter to RawImage and RenderImage (flutter/flutter#185938)
2026-07-17 [email protected] [web] Cache WASM network requests in flutter test (flutter/flutter#189623)
2026-07-17 [email protected] android_hardware_smoke_tests: Apply semantic line breaks to readme (flutter/flutter#189613)
2026-07-16 [email protected] ci(github-actions): resolve zizmor github-env findings in composite flutter actions (flutter/flutter#189602)
2026-07-16 [email protected] Remove obsolete packages analysis flag (flutter/flutter#189525)
2026-07-16 [email protected] Roll Dart SDK from e24870ff15bc to c69d138c9646 (2 revisions) (flutter/flutter#189597)
2026-07-16 [email protected] [fuchsia][iwyu] Remove transitive include of fuchsia.input.report. (flutter/flutter#188891)
2026-07-16 [email protected] Remove unnecessary value key hack in `key.dart`  (flutter/flutter#189291)
2026-07-16 [email protected] Roll Dart SDK from 81306a2ed317 to e24870ff15bc (1 revision) (flutter/flutter#189569)
2026-07-16 [email protected] [Impeller] Fix atlas growth test (flutter/flutter#189533)
2026-07-16 [email protected] Roll Dart SDK from d402ff7c9c84 to 81306a2ed317 (4 revisions) (flutter/flutter#189558)
2026-07-16 [email protected] Move shared darwin plugin tests into own builder (flutter/flutter#189411)
2026-07-16 [email protected] Roll Skia from ab2410bc857c to 37c5e6b26aee (19 revisions) (flutter/flutter#189549)
2026-07-16 [email protected] [ios] Fix //flutter:unittests build for physical devices (flutter/flutter#189543)
2026-07-16 [email protected] Fix lower DragTarget not being recognized in overlapping targets (flutter/flutter#188979)
2026-07-16 [email protected] Relocate cupertino samples that were under widgets/ (flutter/flutter#188876)
2026-07-15 [email protected] [iOS] Migrate DisplayLinkManager to a shared instance (flutter/flutter#189492)
2026-07-15 [email protected] [Impeller] Call glfwTerminate during global test environment teardown if a playground test called glfwInit (flutter/flutter#189523)
2026-07-15 [email protected] Roll pub packages (flutter/flutter#189515)
2026-07-15 [email protected] Roll Packages from ad2eab1 to 9f95026 (8 revisions) (flutter/flutter#189509)
2026-07-15 [email protected] add `@nonVirtual` to `RenderObject.attached`, fix `WidgetTester.hasRunningAnimations` (flutter/flutter#186832)
2026-07-15 [email protected] Roll vulkan-deps to 0582f446e54a (flutter/flutter#188524)
2026-07-15 [email protected] Roll Dart SDK from 0c408ff6dce9 to d402ff7c9c84 (1 revision) (flutter/flutter#189497)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) a: text input Entering text in a text field or keyboard related problems CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-ios iOS applications specifically team-ios Owned by iOS platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants