Skip to content

Fix owner-mismatch crash for OverlayPortal inside a deferred-mount parent (e.g. Table)#185793

Merged
auto-submit[bot] merged 13 commits into
flutter:masterfrom
gabrimatic:overlay-portal-in-table-fix
May 21, 2026
Merged

Fix owner-mismatch crash for OverlayPortal inside a deferred-mount parent (e.g. Table)#185793
auto-submit[bot] merged 13 commits into
flutter:masterfrom
gabrimatic:overlay-portal-in-table-fix

Conversation

@gabrimatic

Copy link
Copy Markdown
Contributor

Issue

Fixes #174133 (and the duplicate #180337). Placing a widget that hosts an OverlayPortal — most visibly Slider, but anything that calls OverlayPortalController.show() before its OverlayPortal is mounted — inside a Table/TableRow crashes during mount with:

'package:flutter/src/rendering/object.dart':
Failed assertion: line 2138 pos 12: 'child.owner == owner': is not true.

#2  RenderObject.redepthChild
#3  _RenderDeferredLayoutBox.redepthChildren           overlay.dart:2576
#4  RenderObject.redepthChild
#5  RenderObject.adoptChild
#6  _RenderTheater._addDeferredChild                   overlay.dart:1312
#7  _OverlayEntryLocation._addChild                    overlay.dart:2185
#8  _OverlayPortalElement.insertRenderObjectChild      overlay.dart:2447
...
#24 _TableElement.mount.<anonymous closure>...        table.dart:303

This is a regression introduced between Flutter 3.32 and 3.35.

Root cause

Table defers calling adoptChild on its RenderObject children until every row has been mounted (_TableElement.insertRenderObjectChild is a no-op while _doingMountOrUpdate; the children are wired in later by _updateRenderObjectChildren). Meanwhile _OverlayPortalElement.mount runs super.mount (which "attaches" the layout surrogate to its parent — but Table swallows it) and then immediately mounts the overlay child, which causes the deferred-layout box to be adopted by the _RenderTheater. The theater is attached, so the deferred box receives an owner — while the layout surrogate has none yet.

When the theater calls redepthChild(deferredBox), the deferred box's redepthChildren unconditionally calls _layoutSurrogate.redepthChild(this), which trips the child.owner == owner assertion across that owner boundary.

Fix

_RenderDeferredLayoutBox.redepthChildren now skips the cross-redepth when the surrogate is not yet attached, mirroring the existing guard in _RenderLayoutSurrogateProxyBox.redepthChildren:

if (_layoutSurrogate.attached) {
  _layoutSurrogate.redepthChild(this);
}

The depth invariant is restored when the surrogate is finally adopted by its parent: _RenderLayoutSurrogateProxyBox.redepthChildren already calls redepthChild(_deferredLayoutChild) once that child becomes attached.

Tests

Adds a regression test in test/widgets/table_test.dart placing an OverlayPortal whose controller is .show()'d before mount (mirroring Slider) inside a TableRow, asserting that no exception is thrown and the overlay child is present.

Without this fix the new test reproduces the assertion. With this fix flutter test test/widgets/table_test.dart, test/widgets/overlay_portal_test.dart, test/widgets/overlay_test.dart, test/widgets/raw_tooltip_test.dart, test/widgets/overlay_layout_builder_test.dart, and test/material/slider_test.dart all pass.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making.
  • All existing and new tests are passing.

@github-actions github-actions Bot added the framework flutter/packages/flutter repository. See also f: labels. label Apr 29, 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 addresses a crash occurring when an OverlayPortal is used within a Table by adding a check to ensure the layout surrogate is attached before calling redepthChild in _RenderDeferredLayoutBox. A regression test has been added to verify the fix. The review feedback suggests enhancing this test by asserting the layout size of the overlay child to ensure the layout phase completes successfully.

Comment thread packages/flutter/test/widgets/table_test.dart Outdated
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label May 1, 2026
@gabrimatic gabrimatic force-pushed the overlay-portal-in-table-fix branch from 141c2b0 to 600e26a Compare May 1, 2026 07:14
@github-actions github-actions Bot removed the CICD Run CI/CD label May 1, 2026
@gabrimatic gabrimatic force-pushed the overlay-portal-in-table-fix branch 3 times, most recently from 0e60bff to 62f231b Compare May 2, 2026 00:38
gabrimatic added 2 commits May 2, 2026 11:06
…rent

`_RenderDeferredLayoutBox.redepthChildren` unconditionally called
`_layoutSurrogate.redepthChild(this)`. When a parent like `Table` defers
adopting its render-object children until every row has been mounted,
the deferred-layout box is adopted by the theater (and gains a pipeline
`owner`) while the layout surrogate is still un-attached, so the
`child.owner == owner` assertion fires.

The opposite direction in `_RenderLayoutSurrogateProxyBox.redepthChildren`
already guards on `child.attached`. Mirror that guard here. The depth
invariant is restored when the surrogate is finally attached, because the
surrogate's own `redepthChildren` re-depths this box.

Adds a regression test placing an `OverlayPortal` (with `controller.show()`
called before mount, mirroring `Slider`) inside a `TableRow` and verifying
neither the assertion nor an exception fires and the overlay child is
present.

Fixes flutter#174133
Fixes flutter#180337
@gabrimatic gabrimatic force-pushed the overlay-portal-in-table-fix branch from 62f231b to 2f52e17 Compare May 2, 2026 09:06
justinmc
justinmc previously approved these changes May 12, 2026

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

Thanks for the PR! LGTM but @LongCatIsLooong should take a look.

Someone wrote a test for this before, do you think it's useful in addition to your test, or leave it out? https://github.com/flutter/flutter/pull/180342/changes

@justinmc justinmc requested a review from LongCatIsLooong May 12, 2026 21:49
@justinmc justinmc added the CICD Run CI/CD label May 12, 2026
@gabrimatic

Copy link
Copy Markdown
Contributor Author

Thanks! I looked at #180342 again. I think the existing test here is the better one to keep because it targets the root OverlayPortal/TableRow owner-ordering issue directly, while still mirroring the Slider path by calling controller.show() before mount. It also now asserts that the overlay child completes layout, so it covers the part that the older Slider regression was trying to protect.

The Slider test is useful as a user-facing reproduction, but adding both feels mostly duplicative and would pull in Material/Slider coverage for a framework-level OverlayPortal fix. So my preference is to leave it out unless @LongCatIsLooong would rather have that higher-level coverage too.

@github-actions github-actions Bot removed the CICD Run CI/CD label May 12, 2026
@Piinks Piinks requested a review from victorsanni May 12, 2026 22:25
@gabrimatic gabrimatic requested a review from justinmc May 12, 2026 23:21

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

That's an elegant fix , thank you!

Comment thread packages/flutter/lib/src/widgets/overlay.dart Outdated
@LongCatIsLooong LongCatIsLooong added the CICD Run CI/CD label May 14, 2026

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

Still LGTM.

Comment thread packages/flutter/test/widgets/table_test.dart Outdated
@github-actions github-actions Bot removed the CICD Run CI/CD label May 14, 2026
@victorsanni victorsanni added the autosubmit Merge PR when tree becomes green via auto submit App label May 21, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 21, 2026
@auto-submit

auto-submit Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

auto label is removed for flutter/flutter/185793, Failed to enqueue flutter/flutter/185793 with HTTP 400: Pull request Required status check "Merge Queue Guard" is expected..

@victorsanni victorsanni added the CICD Run CI/CD label May 21, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label May 21, 2026
@victorsanni victorsanni added CICD Run CI/CD autosubmit Merge PR when tree becomes green via auto submit App labels May 21, 2026
@auto-submit auto-submit Bot added this pull request to the merge queue May 21, 2026
Merged via the queue into flutter:master with commit f9af233 May 21, 2026
94 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 21, 2026
matthewhendrix pushed a commit to matthewhendrix/flutter that referenced this pull request May 23, 2026
…rent (e.g. Table) (flutter#185793)

## Issue

Fixes flutter#174133 (and the duplicate flutter#180337). Placing a widget that hosts
an `OverlayPortal` — most visibly `Slider`, but anything that calls
`OverlayPortalController.show()` before its `OverlayPortal` is mounted —
inside a `Table`/`TableRow` crashes during mount with:

```
'package:flutter/src/rendering/object.dart':
Failed assertion: line 2138 pos 12: 'child.owner == owner': is not true.

flutter#2  RenderObject.redepthChild
flutter#3  _RenderDeferredLayoutBox.redepthChildren           overlay.dart:2576
flutter#4  RenderObject.redepthChild
flutter#5  RenderObject.adoptChild
flutter#6  _RenderTheater._addDeferredChild                   overlay.dart:1312
flutter#7  _OverlayEntryLocation._addChild                    overlay.dart:2185
flutter#8  _OverlayPortalElement.insertRenderObjectChild      overlay.dart:2447
...
flutter#24 _TableElement.mount.<anonymous closure>...        table.dart:303
```

This is a regression introduced between Flutter 3.32 and 3.35.

## Root cause

`Table` defers calling `adoptChild` on its `RenderObject` children until
*every* row has been mounted (`_TableElement.insertRenderObjectChild` is
a no-op while `_doingMountOrUpdate`; the children are wired in later by
`_updateRenderObjectChildren`). Meanwhile `_OverlayPortalElement.mount`
runs `super.mount` (which "attaches" the layout surrogate to its parent
— but Table swallows it) and then immediately mounts the overlay child,
which causes the deferred-layout box to be adopted by the
`_RenderTheater`. The theater is attached, so the deferred box receives
an owner — *while the layout surrogate has none yet*.

When the theater calls `redepthChild(deferredBox)`, the deferred box's
`redepthChildren` unconditionally calls
`_layoutSurrogate.redepthChild(this)`, which trips the `child.owner ==
owner` assertion across that owner boundary.

## Fix

`_RenderDeferredLayoutBox.redepthChildren` now skips the cross-redepth
when the surrogate is not yet attached, mirroring the existing guard in
`_RenderLayoutSurrogateProxyBox.redepthChildren`:

```dart
if (_layoutSurrogate.attached) {
  _layoutSurrogate.redepthChild(this);
}
```

The depth invariant is restored when the surrogate is finally adopted by
its parent: `_RenderLayoutSurrogateProxyBox.redepthChildren` already
calls `redepthChild(_deferredLayoutChild)` once that child becomes
attached.

## Tests

Adds a regression test in `test/widgets/table_test.dart` placing an
`OverlayPortal` whose controller is `.show()`'d before mount (mirroring
`Slider`) inside a `TableRow`, asserting that no exception is thrown and
the overlay child is present.

Without this fix the new test reproduces the assertion. With this fix
`flutter test test/widgets/table_test.dart`,
`test/widgets/overlay_portal_test.dart`,
`test/widgets/overlay_test.dart`, `test/widgets/raw_tooltip_test.dart`,
`test/widgets/overlay_layout_builder_test.dart`, and
`test/material/slider_test.dart` all pass.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making.
- [x] All existing and new tests are passing.

[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo

---------

Co-authored-by: Victor Sanni <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OverlayPortal in Table/TableRow crashes when OverlayPortalController.show is called before the OverlayPortalController is assigned to the OverlayPortal

6 participants