Repository navigation
Description
Steps to reproduce
Release-mode macOS app (engine revision 0cd610717b, Flutter 3.44.8 stable, framework 058e0af2c2), accessibility in use (VoiceOver, an AX client such as a screen recorder or a password manager, or just AppKit's own tracking-area collection on a mouse move). The framework commits a semantics update in which a parent claims a child that has MOVED without sending that child in the same update, the OverlayPortal traversal graft shape described in #182444. Two commits later the app crashes on the main thread in the next accessibility query.
A deterministic unit reproduction on the stock engine at this pin, release config, in accessibility_bridge_unittests.cc: build the tree 0 > [1 > 3 > 4], 2, 5 > 6, then send ONE commit with 1 without child 3, 2 claiming 3, and 5 without child 6 (nodes 3 and 4 not sent). Then walk every delegate's children.
Expected results
Either the update is rejected atomically (the tree and the bridge stay at the previous consistent state) or it is applied consistently (delegates and cached child counts match the tree). Later commits keep applying.
Actual results
AccessibilityBridge::CommitUpdatesfirst removes the child from its old parent, then applies a second update that passesAXTree::ComputePendingChangesbut fails inValidatePendingChangesComplete("Nodes left pending by the update: 3"), AFTERUpdateNodehas already runDeleteOldChildren(nodes freed) and created new nodes.AXTree::Unserializereturns false before the observer notifications and before the unignored-child cache refresh. The bridge therefore keeps delegates whoseax_node_is freed, has no delegate for the new nodes, and leaves parents whose cached unignored child count exceeds their real children.CommitUpdatesgates on the stickyAXTree::error()string (never cleared) instead of the return value, and its first-update error path is anassert(false)that release builds compile out and that returns without clearing the pending updates. Every later commit is refused, so the bridge never recovers.- The next AppKit query dereferences the stale state. Two field signatures from a shipping app, symbolized against the release FlutterMacOS:
- a mouse-moved tracking-area update:
-[FlutterTextField frame](FlutterTextInputSemanticsObject.mm:97) intoFlutterTextPlatformNode::GetFrame(:186) intoAXTree::RelativeToTreeBoundson a freedAXNode. - an out-of-process AX query (
NSAccessibilityChildren):-[AXPlatformNodeCocoa AXChildrenInternal](ax_platform_node_mac.mm:651) intoFlutterPlatformNodeDelegate::ChildAtIndex(flutter_platform_node_delegate.cc:90), fault address0x48(AXNodeData::idread through a null child):GetChildCount()returned the stale cached count andGetUnignoredChildAtIndex(index)returnednullptr.
- a mouse-moved tracking-area update:
In the unit cell: the bridge logs Failed to update ui::AXTree, error: Nodes left pending by the update: 3, the delegate for 6 outlives its node, node 3 has no delegate, node 5 reports one unignored child while GetUnignoredChildAtIndex(0) is null, and the walk crashes with EXC_BAD_ACCESS (code=1, address=0x48), frame 0 ui::AXNode::id(this=0x0) (ax_node.h:110), frame 1 FlutterPlatformNodeDelegate::ChildAtIndex(index=0) (flutter_platform_node_delegate.cc:90). A second cell in FlutterTextInputSemanticsObjectTest.mm, same commit with 6 a text field: the dropped FlutterTextField stays attached to the view and its frame reads the freed node.
A plain move (a node moved AND sent) is safe at this pin: the bridge's two updates delete and recreate the node and replace its delegate.
What we changed locally (happy to send it as a PR)
accessibility_bridge.{h,cc}:CommitUpdatesbranches on theUnserializereturn value, logs the error, and calls a newRecoverFromFailedUpdatethat drops delegates whose node the tree no longer holds, creates delegates for tree nodes that have none, recomputes every node's unignored child cache, and clears the pending updates and queued events. Nothing gates on the sticky error string any more, so later commits apply and fire events normally.flutter_platform_node_delegate.{h,cc}: a delegate whose node lives in its bridge's tree resolves the node by id (a newOwnerBridge::GetNodeFromTree) on every call, and every accessor answers the empty value when the node is gone (0 children, a null child, empty data, an empty rect, no parent, no ancestor, false fromAccessibilityPerformAction).ChildAtIndexguards a nullGetUnignoredChildAtIndex.FlutterTextInputSemanticsObject.mm:FlutterTextPlatformNode::GetFrameanswersNSZeroRectwhen the node is gone.FlutterPlatformNodeDelegateMac.mmandflutter_platform_node_delegate_windows.ccguard their one directGetAXNode()use.
With the patch the two cells pass, common_cpp_unittests passes 234 of 234, and the macOS accessibility and text-field suites pass 12 of 12.
Related: #182444 (the graft shape that produces the update), #187198 (OverlayPortal semantics crashes on macOS).
Code sample
The two unit cells above are the smallest reproduction we have. The app-level trigger is any OverlayPortal graft under an AX client. We can attach the cells as a patch on request.
Logs
Failed to update ui::AXTree, error: Nodes left pending by the update: 3
EXC_BAD_ACCESS (code=1, address=0x48)
0 ui::AXNode::id(this=0x0) ax_node.h:110
1 flutter::FlutterPlatformNodeDelegate::ChildAtIndex(index=0) flutter_platform_node_delegate.cc:90
2 -[AXPlatformNodeCocoa AXChildrenInternal] ax_platform_node_mac.mm:651
Flutter Doctor output
Flutter 3.44.8 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 058e0af2c2 (2026-07-23)
Engine • hash 13ffd72b2f9a5ca4db2a74ea52d5353ec2e8f939 (revision 0cd610717b) (2026-07-23)
Tools • Dart 3.12.2 • DevTools 2.57.0
Activity
mbcorona commentedon Sep 30, 2026
This is distinct from the framework side fix in #187198 / #190431: those stop the corrupt semantics update from being produced, whereas this is the engine accessibility bridge failing to recover from such an update.
Routing to team-accessibility.
[-][macOS] Accessibility bridge keeps freed AXNodes after a failed AXTree update (a parent claims a moved child that is not sent), then crashes in FlutterPlatformNodeDelegate::ChildAtIndex or -[FlutterTextField frame][/-][+][macOS] Accessibility bridge keeps freed AXNodes after a failed AXTree update and crashes on the next AX query[/+]cbracken commentedon Oct 1, 2026
/cc @chunhtai who is probably most familiar with this code.
CanArslanDev commentedon Oct 2, 2026
Small note on the framework side, since this got linked to #190431. That PR does not change the shape described here. I recorded the semantics updates sent to the engine for the #182444 repro and for the MenuAnchor + Tooltip one from that thread, checked them against the rules the bridge applies, and got the same orphan nodes with and without #190431. #190344 was the framework fix for that shape and it was closed without merging, so as far as I can tell nothing on the framework side stops this update from being produced right now.
hannah-hyj commentedon Oct 7, 2026
From triage: I will test offline if this is related to #190357
hannah-hyj commentedon Oct 8, 2026
I think this its related to #190357 but not excatly duplicate.
The root cause is that when there's a framework bug that sends an invalid semantics update, the engine accessibility bridge also doesn't recover after an update fails. On macOS that leads to a crash.
I will try to fix the framework side issue on #190357. but reassign this issue to team-macos for them to decide if an engine side fix is neccessary.
flutter-triage-bot commentedon Oct 13, 2026
The
fyi-macoslabel is redundant with theteam-macoslabel.The
triaged-accessibilitylabel is irrelevant if there is noteam-accessibilitylabel orfyi-accessibilitylabel.This issue is missing a priority label. Please set a priority label when adding the
triaged-macoslabel.vashworth commentedon Oct 14, 2026
@hannah-hyj Were you able to reproduce? Can you add the repo example?
hannah-hyj commentedon Oct 16, 2026
@vashworth Here are the app-level (
main.dart) and engine unit test reproductions:1. Flutter app reproduction (
main.dart)(Note: #193372 will fix the framework side trigger)
Run in release mode (
flutter run -d macos --release— in debug mode,AccessibilityBridge::CreateRemoveReparentedNodesUpdatehitsassert(pending_semantics_node_updates_.find(child_id) != ...)first) with VoiceOver enabled (or another macOS accessibility client active):EXC_BAD_ACCESScrash inFlutterTextPlatformNode::GetFrame/FlutterPlatformNodeDelegate::ChildAtIndexbecauseAXTree::Unserializefreed the removed child node before aborting, leaving stale delegates and cached child counts inAccessibilityBridge.2. Engine unit test reproduction (
accessibility_bridge_unittests.cc)To reproduce deterministically in
engine/src/flutter/shell/platform/common/accessibility_bridge_unittests.cc(in release mode where theassertinCreateRemoveReparentedNodesUpdateis compiled out):hannah-hyj commentedon Oct 16, 2026
since #193372 will fix the framework side trigger , I think we can wait for the PR to land, and then mark the engine side fix as a P2, I'm not even sure the enigne side fix is needed, maybe we should just assume the framework should always be responsible to send correct semantics updates.
LongCatIsLooong commentedon Oct 23, 2026
@amantinband could you check out the main channel and see if the issue persists?
fix(a11y): stop sliders from freezing the desktop accessibility tree (#…
github-actions commentedon Nov 13, 2026
Without additional information, we are unfortunately not sure how to resolve this issue. We are therefore reluctantly going to close this bug for now.
If you find this problem please file a new issue with the same description, what happens, logs and the output of 'flutter doctor -v'. All system setups can be slightly different so it's always better to open new issues and reference the related ones.
Thanks for your contribution.