Skip to content

Do not merge semantics configs anchoring different traversal parents - #190344

Closed
Nael-Sayegh wants to merge 2 commits into
flutter:masterfrom
Nael-Sayegh:fix-traversal-parent-merge-collision
Closed

Nael-Sayegh wants to merge 2 commits into
flutter:masterfrom
Nael-Sayegh:fix-traversal-parent-merge-collision

Conversation

@Nael-Sayegh

@Nael-Sayegh Nael-Sayegh commented Jul 31, 2026 •

Copy link
Copy Markdown

Fixes #182444

Draft: the CLA is not signed yet. Opening early so the analysis is on the record; I'll mark it ready once that's done.

The bug

SemanticsConfiguration.isCompatibleWith guards _traversalChildIdentifier but not _traversalParentIdentifier:

if (_traversalChildIdentifier != other._traversalChildIdentifier) {
  return false;
}

So two sibling anchors — for example two adjacent Tooltips, each backed by its own OverlayPortal — are considered mergeable. absorb then keeps only the first identifier, because it assigns with ??=:

if (_traversalChildIdentifier == null) {
  _traversalParentIdentifier ??= child._traversalParentIdentifier;
}

The second one is silently dropped. Since that identifier is the only handle its traversal children have to find their parent, the second portal's overlay child resolves no traversal parent (traversalParent = -1) and is serialized as an orphan — no node claims it as a child.

On Windows ui::AXTree::Unserialize then rejects the entire update with N will not be in the tree and is not the new root (ax_tree.cc:1346). After that the accessibility tree stays frozen for the life of the window, so NVDA keeps presenting a stale screen and the app becomes untraversable.

Note the engine cannot defend against this: accessibility_bridge.cc copies children_in_traversal_order straight into AXNodeData::child_ids without validating or pruning, so an inconsistent SemanticsUpdate is passed through as-is.

Why a ListView is needed to reproduce

Each ListView item is wrapped in an IndexedSemantics, which absorbs everything it contains into a single node — that is what forces the two anchors together. The same two anchors in a bare Row stay separate, which is why the issue only reproduces inside a list. Neither Tooltip nor hovering is actually required: two bare Semantics(traversalParentIdentifier: ...) widgets in a list item are enough.

This also corrects the root-cause analysis in the issue itself, which attributes the bug to detachment ordering during sendSemanticsUpdate. That hypothesis is not what happens here — the identifier never reaches a node in the first place. The two related fixes that landed in May (#186118, #186826) do not cover this case; I verified the issue still reproduces on master.

The fix

Refuse to merge two configurations that each anchor a different, non-null traversal parent. The guard is deliberately narrower than the traversalChildIdentifier one above it: a plain != would also stop an anchor from merging with ordinary unannotated content, which would change tree shape broadly for no benefit.

Tests

Added sibling traversal parents are not merged into a single node in packages/flutter/test/widgets/semantics_test.dart, next to the existing grafting tests. It fails without the fix:

Expected: contains all of ['A', 'B']
  Actual: ['A']

and passes with it.

Also verified against the issue's own reproduction, using a harness that replays the engine's bridge contract in Dart (every serialized node must end up reachable from the root): fails on stable 3.44.8 and on unpatched master with the exact production error text, passes with the fix, including a 5-tooltip variant.

No regressions: test/widgets (7174 passed), test/material (7973 passed), plus test/semantics, test/widgets/semantics_test.dart, test/widgets/overlay_portal_test.dart and test/material/tooltip_test.dart.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the AI contribution guidelines and understand my responsibilities. This change was developed with AI assistance (disclosed via Co-Authored-By on the commit); the analysis, the reproduction and the test results above were verified by running them.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making.
  • I followed the breaking change policy and added Data Driven Fixes where supported.
  • All existing and new tests are passing.

Fixes flutter#182444

`SemanticsConfiguration.isCompatibleWith` guarded `_traversalChildIdentifier`
but not `_traversalParentIdentifier`, so two sibling anchors -- for example two
adjacent `Tooltip`s, each backed by its own `OverlayPortal` -- were considered
mergeable. `absorb` then kept only the first identifier, since it assigns with
`??=`, and silently dropped the second.

The dropped identifier is the only handle its traversal children have to find
their parent, so the second portal's overlay child resolved no traversal parent
and was serialized as an orphan. On Windows `ui::AXTree::Unserialize` rejects
the whole update with "N will not be in the tree and is not the new root",
after which the accessibility tree stays frozen for the life of the window and
NVDA can no longer traverse the app.

A `ListView` is what forces the merge in practice: each list item is wrapped in
an `IndexedSemantics`, which absorbs everything it contains into a single node.
The same two anchors in a bare `Row` stay separate, which is why the issue only
reproduces inside a list.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@github-actions github-actions Bot added framework flutter/packages/flutter repository. See also f: labels. a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) labels Jul 31, 2026
@google-cla

google-cla Bot commented Jul 31, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@Nael-Sayegh
Nael-Sayegh marked this pull request as ready for review July 31, 2026 10:56

@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 modifies SemanticsConfiguration to prevent the merging of two configurations that anchor different traversal parents by ensuring they are not considered compatible if they have different non-null _traversalParentIdentifier values. A corresponding regression test has been added to verify this behavior. I have no feedback to provide.

@Renzo-Olivares

Copy link
Copy Markdown
Contributor

@Nael-Sayegh before we can move forward with this PR you will need to sign the CLA. Thank you!

// two sibling OverlayPortals) must not be merged: [absorb] can only keep
// one `traversalParentIdentifier`, so the other one would be dropped and
// its traversal children would end up grafted to nothing.
if (_traversalParentIdentifier != null &&

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.

We should follow the same _traversalChildIdentifier that just check for inequilty, this is to ensure they don't merge up at all to create weird situation

@Renzo-Olivares

Copy link
Copy Markdown
Contributor

Hi @Nael-Sayegh, friendly reminder that you need to sign the CLA before moving forward with the PR.

@Renzo-Olivares Renzo-Olivares added the waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds label Sep 8, 2026
@github-actions

Copy link
Copy Markdown

This pull request is being closed because it has not been updated in the last 21 days after a request for more information.

If you are still working on this, please feel free to reopen it or file a new pull request.

Thanks for your contribution.

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) CICD Run CI/CD framework flutter/packages/flutter repository. See also f: labels. waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows ListView+Tooltip can trigger error: Failed to update ui::AXTree, error: 9 will not be in the tree and is not the new root

4 participants