Repository navigation
Description
Steps to reproduce
I'm having trouble getting a consistent replication. I'll share two videos to demonstrate. This bug seems to be related to traversalParentIdentifier, and may be related to #182604. I'm not sure if this is relevant, but I'm running on macOS 15.5
- Run example on macOS
- Turn on VoiceOver
- Open the menu
Expected results
Accessibility focus moves to overlay and buttons and shows the correct semantic boxes around overlay items.
Actual results
Either the AXTree completely crashes, or the semantics rectangles are not properly positioned. Yesterday, the semantics tree always crashed. I'm not sure why it isn't crashing today.
Also, the semantics debugger is not showing the overlay tree. This is due to:
flutter/packages/flutter/lib/src/widgets/semantics_debugger.dart
Lines 340 to 342 in 21c6566
| if (node.traversalChildIdentifier != null) { | |
| return; | |
| } |
Removing this line shows the correct semantics, so I'm not sure what the issue was:
Code sample
Code sample
First video code snippet:
import 'package:flutter/material.dart';
void main() {
runApp(
const MaterialApp(
showSemanticsDebugger: true
home: Bug()
)
);
}
class Bug extends StatelessWidget {
const Bug({super.key});
@override
Widget build(BuildContext context) {
final controller = OverlayPortalController();
return Scaffold(
body: SizedBox(
width: 50,
height: 50,
child: OverlayPortal(
controller: controller,
// The massive overlay child is forced to be a logical child of the tiny 50x50 box.
// This destroys the globalRect matrix and crashes ui::AXTree.
overlayChildBuilder: (context) {
return Positioned(
top: 50,
left: 50,
child: Semantics(
label: 'I will not show in the debugger and crash AXTree',
child: Container(width: 300, height: 300, color: Colors.red),
),
);
},
child: TextButton(onPressed: controller.toggle, child: const Text('Open')),
),
),
);
}
}Second video code snippet:
void main() => runApp(const SimpleCascadingMenuApp());
/// A Simple Cascading Menu example using the [MenuAnchor] Widget.
class MyCascadingMenu extends StatefulWidget {
const MyCascadingMenu({super.key});
@override
State<MyCascadingMenu> createState() => _MyCascadingMenuState();
}
class _MyCascadingMenuState extends State<MyCascadingMenu> {
final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');
@override
void dispose() {
_buttonFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MenuAnchor(
childFocusNode: _buttonFocusNode,
menuChildren: <Widget>[
MenuItemButton(onPressed: () {}, child: const Text('Revert')),
MenuItemButton(onPressed: () {}, child: const Text('Setting')),
MenuItemButton(onPressed: () {}, child: const Text('Send Feedback')),
],
builder: (_, MenuController controller, Widget? child) {
return IconButton(
focusNode: _buttonFocusNode,
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.more_vert),
);
},
);
}
}
/// Top Level Application Widget.
class SimpleCascadingMenuApp extends StatelessWidget {
const SimpleCascadingMenuApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('MenuAnchor Simple Example'),
leading: const MyCascadingMenu(),
),
),
);
}
}Screenshots or Video
Screenshots / Video demonstration
Video 1:
Screen.Recording.2026-05-27.at.3.55.33.PM.mov
Video 2:
Screen.Recording.2026-05-27.at.4.06.16.PM.mov
Logs
The only relevant log is the AXTree drop. I experienced this on every menu yesterday, regardless of whether the semantics debugger was enabled. I'm having trouble replicating it on every run today.
Logs
[ERROR: flutter/shell/platform/common/accessibility_bridge.cc(114)] Failed to update ui::AXTree, error: 4 will not be in the tree and is not the new rootFlutter Doctor output
Doctor output
[✓] Flutter (Channel master, 3.45.0-1.0.pre-201, on macOS 15.5 24F74 darwin-arm64, locale en-US) [1,681ms]
• Flutter version 3.45.0-1.0.pre-201 on channel master at /Users/davidhicks/fvm/versions/master
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 40bfc7cac5 (2 days ago), 2026-05-25 11:59:40 -0400
• Engine revision 40bfc7cac5
• Dart version 3.13.0 (build 3.13.0-134.0.dev)
• DevTools version 2.58.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets,
enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration, enable-riscv64
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [2.3s]
• Android SDK at /Users/davidhicks/Library/Android/sdk
• Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A)
• Platform android-36.1, build-tools 36.1.0
• ANDROID_HOME = /Users/davidhicks/Library/Android/sdk
• Java binary at: /Users/davidhicks/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home/bin/java
This JDK is specified in your Flutter configuration.
To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment JBR-17.0.14+1-1367.22-nomod (build 17.0.14+1-b1367.22)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 16.4) [1,612ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 16F6
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [6ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (2 available) [6.8s]
• macOS (desktop) • macos • darwin-arm64 • macOS 15.5 24F74 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 148.0.7778.179
! Error: Browsing on the local area network for David’s iPhone. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources [257ms]
• All expected network resources are available.
• No issues found!
Activity
[-][widgets/overlay_portal.dart] Strange OverlayPortal behavior on MacOS.[/-][+][widgets/overlay_portal.dart] OverlayPortal semantics tree crashes or behaves unexpectedly on MacOS.[/+]QuncCccccc commentedon May 28, 2026
Seems like this is only on macOS. Android and iOS can traverse the items correctly. Maybe the transform is calculated incorrectly on MacOS engine side?
I actually cannot recall the details why I added the code. I vaguely remember removing this might cause failures in g3.
flutter/packages/flutter/lib/src/widgets/semantics_debugger.dart
Lines 340 to 342 in 21c6566
davidhicks980 commentedon May 28, 2026
@QuncCccccc I think you're right. I'm not familiar enough with the engine to determine what is causing it, but from what I could gather macOS may be clipping the overlay's semantic bounds to the child's bounds because the overlay is the semantic child? Not sure.
puneetkukreja98 commentedon May 28, 2026
@davidhicks980 Thanks for reporting the issue.
Observations:
First video code snippet
When
showSemanticsDebuggeris set totrueand VoiceOver is enabled, clicking the button throws the following error:The error is not occurring consistently. I was able to reproduce it after trying 2–3 times while checking the logs.
Also,
Ctrl + Option + Spaceis not working in this scenario.When
showSemanticsDebuggeris set tofalse, the red container renders correctly while VoiceOver is enabled.Second video code snippet
When
showSemanticsDebuggeris set tofalseand VoiceOver is enabled,Ctrl + Option + Spaceis working.However, using
Ctrl + Option + Right Arrow/Left Arrowdoes not move focus to the menu item buttons (Revert,Settings,Send Feedback), even though VoiceOver is announcing them, as shown in above video.When
showSemanticsDebuggeris set totrue, both button click andCtrl + Option + Spaceare not working.Routing this to team-accessibility for further investigation.
3 remaining items
QuncCccccc commentedon Jun 22, 2026
Can you try whether the same issue still happens on master? I landed a PR #186826 about 3 weeks ago but seems it is not in 3.44.0.
NL-CanArslan commentedon Jul 6, 2026
Quick follow-up from our side, we hit this again on 3.44.2 stable with a different and much easier repro path.
We have an OverlayPortal used as an anchored popup: CompositedTransformTarget on the trigger, and the overlayChildBuilder returns a full screen dismiss barrier plus a CompositedTransformFollower panel. If you spam tap the trigger so the portal gets shown and hidden repeatedly in a short time, the semantics tree gets corrupted and every frame after that logs this cascade until the screen is rebuilt:
Still no accessibility service involved, semantics is enabled by Sentry's user interaction tracing like I mentioned above.
Two data points that might help narrow it down:
I have not been able to run the full app against master yet, so I cannot say whether #186826 covers this path. If a minimal repro would help I can try to extract one from the popup above.
NL-CanArslan commentedon Jul 6, 2026
I managed to reduce this to a self-contained reproduction with no app-specific code. It is a single
main.dartplusgo_routeras the only extra dependency, and it throws the same three assertions.Environment
Debug build. Reproduced on the iOS Simulator and on device.
Assertions
Stack (entirely inside the framework, no application frames)
What triggers it
Five things have to be present at the same time. Removing any one of 1, 4 or 5 makes the crash go away:
SemanticsBinding.instance.ensureSemantics(). Turning on VoiceOver/TalkBack has the same effect.OverlayPortalwhose overlay child grafts a subtree throughCompositedTransformFollowerand rebuilds once after first layout (measure, then reveal with anOpacity0 to 1 and aTransform.translatein a post-framesetState).OverlayEntryinserted on the root overlay, wrapping a nestedNavigatorand anOverlayRoute.Offstage(its semantics dropped) while all of this happens.Steps
Run in debug, press "Open overlay", then rapidly tap one of the anchor circles a few times and jump between them. Within a second or two it starts throwing every frame.
pubspec.yaml
main.dart
QuncCccccc commentedon Jul 8, 2026
Hi @NL-CanArslan, please try whether master branch fixes the problem. As I mentioned above, I merged c7f49fe several weeks ago but seems like it is not included in 3.44.2 stable.
NL-CanArslan commentedon Jul 10, 2026
Thanks for the pointer. I gave master a try and the crash still shows up with the same repro I posted above.
I double checked that #186826 is actually in this checkout:
So the fix is there, but the assertions still fire. The line numbers just moved a bit. Same stack as before, all inside the framework:
Looks like #186826 does not cover this particular case. Happy to test any follow up patch whenever you have one.
CanArslanDev commentedon Aug 3, 2026
Picking up from the reports I posted earlier in this thread under my other account (@NL-CanArslan). Rather than collecting more stack traces I went digging in the framework source this time, and I think the actual defect is a three line asymmetry in
SemanticsNode.All line numbers below are from 3.44.8 stable (
058e0af2c2), and they happen to match the numbers in my earlier reports exactly.detach()guards against a stale child list,attach()does notdetach()knows the_childrenlist can be out of date, and says so in a comment:attach()walks the same list with no such check:So when a node carrying a stale list gets attached, it drags along nodes that no longer belong to it. The result is a node with
attached == trueandparent == null, and every assertion in this issue and in #189902 falls out of that one state:_owner == nullatsemantics.dart:3246, when the orphan gets attached a second time under its real parent!child.attachedatsemantics.dart:3008, when something later tries to legitimately adopt itnode.builtatobject.dart:5713, when the flush ends with the_RenderObjectSemanticstree still dirtyWhy the list goes stale and never gets cleaned up
_adoptChildand_dropChilddeliberately do not touch the old parent's_children. That is normally harmless, because the old parent overwrites the entire list at the end of its own update (_children = newChildren, semantics.dart:3080).That overwrite never happens if the old parent's
_RenderObjectSemanticsis still markedbuilt, because_buildSemanticsshort circuits before it can reach_buildSemanticsSubtree:And nothing marks it as needing a build when one of its semantics children is stolen.
markNeedsBuild(object.dart:5748) only reacts to config, geometry and parentData changes, and there is no back reference from aSemanticsNodeto the_RenderObjectSemanticsthat cached it. MeanwhilecachedSemanticsNodekeeps the node alive across hide and show cycles, stale list and all.I think this is the piece @xiangh-2018 could not account for in #189902 ("for some reason (i can't explain), n#6 is not rebuilt").
Why it is always OverlayPortal
The "child was stolen by a deeper node" branch in
_replaceChildren(semantics.dart:3000-3006) is not an exotic edge case, it is exactly how OverlayPortal grafting works._OverlayPortalState.buildwraps the anchor inSemantics(traversalParentIdentifier: this, ...)(overlay.dart:2095 and 2115), the deferred overlay child setsconfig.traversalChildIdentifier(overlay.dart:2694), andSemanticsOwner._traversalParentNodes/_traversalChildNodes(semantics.dart:4995 and 4998) then re-parent the node in traversal order while hit test order keeps the paint parent. One node, two parents, in two subtrees that rebuild at different times.Worth noting that
RawTooltipis built onOverlayPortal.overlayChildLayoutBuilder(raw_tooltip.dart:886), so the Tooltip repro in #189902 is an OverlayPortal repro. Those two issues are not just hitting the same assertion, they are on the same code path.The AXTree error looks like the same bug in release mode
@davidhicks980's
Failed to update ui::AXTree, error: N will not be in the tree and is not the new rootcomes from the same orphan node. The grafting code usesattachedas its liveness filter:A spuriously attached orphan passes that filter and gets written into
childrenInTraversalOrderas an id the engine cannot reach. The assertions are debug only, this one is not.Regression window
The traversal grafting machinery reached stable in 3.41.0. That lines up with the "worked on 3.38.5, broke on 3.44.5" report in #189902, and
fccfa978a97is the commit named in the title of #182604. All three issues sit downstream of the same refactor.Candidate fix
Mirror the guard that
detach()already has:if (_children != null) { for (final SemanticsNode child in _children!) { - child.attach(owner); + // The list of children may be stale and may contain nodes that have + // been assigned to a different parent. + if (child.parent == this) { + child.attach(owner); + } } }This looks safe by construction to me.
_adoptChildsetschild._parent = thisbefore it callschild.attach(_owner!)(semantics.dart:3221), so during a genuine recursive attach every real child already satisfies the guard.Repro as a widget test
I got this down to a headless widget test with no Tooltip, no hover, no timers and no extra packages. Five ingredients are needed, and the one that surprised me is the second anchor: with a single
OverlayPortalI could not reproduce it at all. In @xiangh-2018's log the node that actually gets stolen belongs to the sibling anchor, not to the one being toggled, and my reduction only started failing once I made the sibling a portal anchor too.This fails on 3.44.8 stable with
'!child.attached': is not trueat semantics.dart:3008 and passes with the guard applied. A Tooltip flavoured version of #189902's repro behaves identically.Test results with the patch applied
packages/flutter/test/semantics/plustest/rendering/: 892 passed, nothing failedpackages/flutter/test/widgets/: 7014 passed, 15 failed. Those 15 are golden image mismatches intext_golden_test.dartandbox_decoration_test.dart, and they fail identically on an unpatched checkout on this machine, so they are local noise rather than a regression.What this does not fix
The guard makes
attachedmean what the grafting code already assumes it means, and it stops the corruption from reaching the engine, but it is still treating a symptom. A_RenderObjectSemanticswhosecachedSemanticsNodehad a child stolen keeps a stale child list and stale geometry either way. The real fix is invalidating that cache at the moment of the theft, which needs either a back reference fromSemanticsNode, or invalidation on both ends of the graft in the OverlayPortal teardown path. My guess is that theidentical(childRenderObject, parentRenderObject)assertion at object.dart:6670 that @madenvel reported in #189902 is exactly that missing piece, and the guard will not help there. I have not tested that one.One note on the patch @xiangh-2018 proposed in #189902: removing the child from
p._childrenbefore dropping it does fix the dangling reference, but it works against the invariant the framework documents indetach(), and_childrencan be theconst <SemanticsNode>[]thatupdateWithsubstitutes when the caller passes null (semantics.dart:3750), whereremovewould throw.@QuncCccccc happy to open a PR with the guard and the two tests if that is useful, or to run any alternative patch against the repro.
xiangh-2018 commentedon Aug 4, 2026
Hello @CanArslanDev , as you said in #189902 (comment), the proposed check is defensive. And to my taste it's always error prone to leave dangling stale refs. I agree a real fix would be more involving.
CanArslanDev commentedon Aug 4, 2026
@xiangh-2018 that is a fair objection, and having gone and counted I think you are more right than my patch admits. These are the places in
semantics.darton master that walk_children:_replaceChildren, two loopsvisitChildren_visitDescendants_childrenInHitTestOrder_updateChildrenInTraversalOrderSemanticsOwner._getSemanticsActionHandlerForPositionattachdetachOnly
detachcheckschild.parent == thistoday, and my patch takes that to two out of eight. Two of the unguarded ones feed ids straight to the engine. So "stale entries are tolerated" is an invariant that most of the file does not actually honour, and adding a seventh place that has to remember the rule is not a good long term shape. I agree with you there.Where I would still push back is on the patch as written:
_childrenis the list instance the caller handed toupdateWith, andupdateWithsubstitutesconst <SemanticsNode>[]whenchildrenInInversePaintOrderis null (semantics.dart:3750 on 3.44.8). I assumedremovewould only throw when it actually finds something to remove, but that is not how it behaves:It rejects unconditionally, so this is a crash rather than just an ownership nit.
A copy on write version avoids that and still gets you what you want, which is no dangling refs left behind:
I tried this on master with my
attachguard removed entirely, so it was doing the work on its own. It fixes the regression test in #190431, andtest/semantics,test/renderingandtest/widgetscome out at 8095 passing with nothing failing. The cost is one list copy on the theft path, which is rare.So both shapes work and neither regresses anything I can find. If reviewers prefer yours I am happy to switch the PR over to it, or carry both, since they are addressing different halves of the same invariant.
Neither one is the real fix though, as you say. After the theft the cached
_RenderObjectSemanticsstill holds a stale node and stale geometry regardless of which patch is in, so I would expect @madenvel'scomputeChildGeometryassertion to survive both. That one needs the invalidation, and I am happy to take that on if the team wants to go straight there instead.xiangh-2018 commentedon Aug 5, 2026
Hello @CanArslanDev, thanks for your hard work. I checked far less code paths than you did, and my fix is more a PoC than real fix. Yours is safer.
I can not connect the assertion that @madenvel reported in #189902 with this, because that looks like a bad RenderObject/_RenderObjectSemantics parent/child relation, while this is, superficially, a bad SemanticsNode p/c relation.
I guess you are right about "The real fix is invalidating that cache at the moment of the theft", because in my test if the SemanticsNode whose child has been stolen, is rebuilt, the assertion won't fire. I haven't looked into the OverlayPortal teardown path which looks suspicious as per your comments.
CanArslanDev commentedon Aug 5, 2026
@xiangh-2018 thanks, and it turns out you were pointing at the right thing all along. I went and prototyped the invalidation before replying, and it does not work, so I have updated #190431 to go your way instead.
The problem is that the theft always happens inside
flushSemantics, and the framework blocks dirtying semantics from there in three separate places:markNeedsBuild()on the old parent's_RenderObjectSemanticsleaves the flush with a dirty node, sodebugCheckForBuildstripsassert(node.built)RenderObject.markNeedsSemanticsUpdate()asserts!attached || !owner!._debugDoingSemanticsPipelineOwner.flushSemanticsasserts_nodesNeedingSemanticsUpdate.isEmptyon the way outI tried the first two against the repro. Both fire. The flush is built to reach a fixpoint in one pass, so "invalidate now, rebuild later" has nowhere to put the later. The repair has to be synchronous, which is what your patch was doing.
An aside that amused me: the naive
markNeedsBuildversion fails withassert(node.built), which is one of the three assertion variants originally reported in this issue. So that variant is probably what it looks like when something dirties semantics mid flush.The other thing I checked is where stale entries can come from at all.
_dropChildhas exactly two callers, both in_replaceChildren. The one in the dead child loop is harmless because that node replaces its whole list a few lines later. The one in the steal branch is the only place that can leave a stale entry behind. So repairing it there closes the source completely, and your instinct that dangling refs should not be left lying around was the correct read.The PR now does both.
_replaceChildrenrepairs the old parent's list when it takes a child, replacing the list rather than mutating it so the const case cannot throw, andattachkeeps the guard as hardening for the other readers you made me go count. Either one fixes the repro on its own, and everything stays green with both.On @madenvel's assertion, I think your reading is right and mine was sloppy. It is a
RenderObjectto_RenderObjectSemanticsrelationship going bad, not aSemanticsNodeparent and child one, and nothing here touches that. It probably wants its own issue rather than being folded into this one.CanArslanDev commentedon Sep 6, 2026
Coming back to correct two things I said in this thread, because both turned out to be wrong and both of them point away from the right answer.
Invalidating the cache is possible. In my comment above I said I had prototyped it and it could not be done, because the theft always happens inside
flushSemanticsandmarkNeedsBuild,RenderObject.markNeedsSemanticsUpdateand theflushSemanticsexit assert all refuse to be dirtied from there. Those three asserts are real, but the conclusion was too broad. It is the moment of the theft that cannot be used, because that is the middle of the build phase. The decision that leads to the theft is made somewhere else:_hasSiblingConflictis set in_marksConflictsInMergeGroup, which runs fromupdateChildrenduring the parent data phase, before the build loop. Marking there is picked up in the same flush and none of the three fire.@chunhtai made this point on the PR and was right about it. The concrete gap is that
shouldFormSemanticsNodeends inparentData!.explicitChildNodes || _hasSiblingConflictand only the first half is invalidated._didUpdateParentDatacallsmarkNeedsBuildwith the comment "Parent data changes may result in node formation changes".markSiblingConfigurationConflictassigns the flag and returns. So a fragment can stop forming a node, keepbuiltand itscachedSemanticsNode, have its children taken while it is out of the tree, and be handed back in still holding them. #190431 now marks the fragment when that flag changes, and both of the patches I was arguing about here are gone.#182604 is not the same defect. I said here, and again on #189902, that this issue, #189902 and #182604 all come down to one thing. #182604 was fixed a few days ago by #192139, and it was an engine side iOS bug:
setChildrenInHitTestOrderwas mutatingchild.parent, soOverlayoverwrote the traversal parent thatOverlayPortalhad just set, and the nodes ended up anchored to the wrong origin. Both sit downstream offccfa978a97splitting traversal order from hit test order, but that is where the shared history ends. "After the same commit" is not the same as "the same bug" and I should not have written it that way.One smaller correction while I am here. I listed eight places in
semantics.dartthat walk_children. It is ten._redepthChildrenand_updateChildrenMergeFlagswalk it too, and both assert onchild.owner, so a stale entry can trip in those as well.detachis still the only one that checks whether the child is actually still its own.Rebuild a semantics fragment when its sibling conflict changes (flutt…
Rebuild a semantics fragment when its sibling conflict changes (flutt…
flutter-triage-bot commentedon Nov 11, 2026
This issue is assigned to @QuncCccccc but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!