From 7e726e05079d546b6ea6673406cf11922a6444be Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 8 Jun 2026 07:49:55 -0700 Subject: [PATCH] Fix `display: contents` nodes having `hasNewLayout` set incorrectly (#57103) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/57103 Changelog: [General][Fixed] Fixed `display: contents` nodes having `hasNewLayout` set incorrectly `cleanupContentsNodesRecursively` unconditionally sets `hasNewLayout=true` on `display: contents` children, including on code paths where their parent's layout was not actually performed in this pass. The stale flag can survive across layout passes and, in clone-on-write renderers (e.g. React Native Fabric), be observed by a subsequent pass whose parent was cloned but whose layout was served from cache, leaving the contents child's owner pointing at the previous parent revision. There are two paths through which the cleanup could stamp a contents child whose parent's `hasNewLayout` would end up false: 1. Measure-phase visit. Inside `calculateLayoutImpl`, the cleanup ran with no knowledge of `performLayout`. When the parent's `calculateLayoutImpl` was invoked only with `performLayout=false` (cache miss on measure, cache hit on layout), the cleanup stamped contents children even though the parent itself never had its `hasNewLayout` set. 2. Absolute-layout walk. `layoutAbsoluteDescendants` walks every static layout descendant of the containing block - including ones whose own `calculateLayoutImpl` was skipped via the layout-phase cache. The cleanup invoked along that walk unconditionally stamped contents children, but the parent's `hasNewLayout` was only updated when the recursion actually found new layout downstream. In both cases, the result is the same invariant violation: a contents node with `hasNewLayout=true` whose parent has `hasNewLayout=false`. A consumer iterating the tree via `hasNewLayout` skips the parent and never clears the stale flag. X-link: https://github.com/facebook/yoga/pull/1970 Test Plan: Added `YGContentsNodeHasNewLayoutTest.cpp` with regression tests: - `contents_child_hasNewLayout_not_stamped_on_measure_only_visit` - pins the measure-phase fix - `absolute_descendant_through_contents_is_reachable_via_hasNewLayout` - pins the positive case for absolute-layout path - `absolute_phase_cleanup_does_not_stamp_when_parent_layout_skipped` - pins the negative case for absolute-layout path Reviewed By: javache Differential Revision: D107854528 Pulled By: j-piasecki fbshipit-source-id: cae5e889622296e8b6380a6428509b5ffea3e9ae --- .../yoga/yoga/algorithm/AbsoluteLayout.cpp | 3 ++- .../yoga/yoga/algorithm/CalculateLayout.cpp | 18 +++++++++++------- .../yoga/yoga/algorithm/CalculateLayout.h | 2 +- .../api-snapshots/ReactAndroidDebugCxx.api | 2 +- .../api-snapshots/ReactAndroidReleaseCxx.api | 2 +- .../api-snapshots/ReactAppleDebugCxx.api | 2 +- .../api-snapshots/ReactAppleReleaseCxx.api | 2 +- .../api-snapshots/ReactCommonDebugCxx.api | 2 +- .../api-snapshots/ReactCommonReleaseCxx.api | 2 +- 9 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/AbsoluteLayout.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/AbsoluteLayout.cpp index 4f14165d7e9..a7d15773fc6 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/AbsoluteLayout.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/AbsoluteLayout.cpp @@ -558,7 +558,6 @@ bool layoutAbsoluteDescendants( // we need to mutate these descendents. Make sure the path of // nodes to them is mutable before positioning. child->cloneChildrenIfNeeded(); - cleanupContentsNodesRecursively(child); const Direction childDirection = child->resolveDirection(currentNodeDirection); // By now all descendants of the containing block that are not absolute @@ -584,6 +583,8 @@ bool layoutAbsoluteDescendants( containingNodeAvailableInnerHeight) || hasNewLayout; + cleanupContentsNodesRecursively( + child, /* didPerformLayout */ hasNewLayout); if (hasNewLayout) { child->setHasNewLayout(hasNewLayout); } diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp index 6c90ddce39c..e45a779f875 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp @@ -506,7 +506,9 @@ void zeroOutLayoutRecursively(yoga::Node* const node) { } } -void cleanupContentsNodesRecursively(yoga::Node* const node) { +void cleanupContentsNodesRecursively( + yoga::Node* const node, + bool didPerformLayout) { if (node->hasContentsChildren()) [[unlikely]] { node->cloneContentsChildrenIfNeeded(); for (auto child : node->getChildren()) { @@ -514,11 +516,13 @@ void cleanupContentsNodesRecursively(yoga::Node* const node) { child->getLayout() = {}; child->setLayoutDimension(0, Dimension::Width); child->setLayoutDimension(0, Dimension::Height); - child->setHasNewLayout(true); + if (didPerformLayout) { + child->setHasNewLayout(true); + } child->setDirty(false); child->cloneChildrenIfNeeded(); - cleanupContentsNodesRecursively(child); + cleanupContentsNodesRecursively(child, didPerformLayout); } } } @@ -1360,7 +1364,7 @@ static void calculateLayoutImpl( // Clean and update all display: contents nodes with a direct path to the // current node as they will not be traversed - cleanupContentsNodesRecursively(node); + cleanupContentsNodesRecursively(node, performLayout); return; } @@ -1378,7 +1382,7 @@ static void calculateLayoutImpl( // Clean and update all display: contents nodes with a direct path to the // current node as they will not be traversed - cleanupContentsNodesRecursively(node); + cleanupContentsNodesRecursively(node, performLayout); return; } @@ -1396,7 +1400,7 @@ static void calculateLayoutImpl( ownerHeight)) { // Clean and update all display: contents nodes with a direct path to the // current node as they will not be traversed - cleanupContentsNodesRecursively(node); + cleanupContentsNodesRecursively(node, /* didPerformLayout */ false); return; } @@ -1408,7 +1412,7 @@ static void calculateLayoutImpl( // Clean and update all display: contents nodes with a direct path to the // current node as they will not be traversed - cleanupContentsNodesRecursively(node); + cleanupContentsNodesRecursively(node, performLayout); // STEP 1: CALCULATE VALUES FOR REMAINDER OF ALGORITHM const FlexDirection mainAxis = diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.h b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.h index 2314bce0e46..0cc7c678949 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.h +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.h @@ -55,6 +55,6 @@ float calculateAvailableInnerDimension( void zeroOutLayoutRecursively(yoga::Node* const node); -void cleanupContentsNodesRecursively(yoga::Node* const node); +void cleanupContentsNodesRecursively(yoga::Node* node, bool didPerformLayout); } // namespace facebook::yoga diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 46bf19be661..eef950f0131 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -12822,7 +12822,7 @@ void facebook::yoga::assertFatal(bool condition, const char* message); void facebook::yoga::assertFatalWithConfig(const facebook::yoga::Config* config, bool condition, const char* message); void facebook::yoga::assertFatalWithNode(const facebook::yoga::Node* node, bool condition, const char* message); void facebook::yoga::calculateLayout(facebook::yoga::Node* node, float ownerWidth, float ownerHeight, facebook::yoga::Direction ownerDirection); -void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node *const node); +void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node* node, bool didPerformLayout); void facebook::yoga::constrainMaxSizeForMode(const facebook::yoga::Node* node, facebook::yoga::Direction direction, facebook::yoga::FlexDirection axis, float ownerAxisSize, float ownerWidth, facebook::yoga::SizingMode* mode, float* size); void facebook::yoga::fatalWithMessage(const char* message); void facebook::yoga::layoutAbsoluteChild(const facebook::yoga::Node* containingNode, const facebook::yoga::Node* node, facebook::yoga::Node* child, float containingBlockWidth, float containingBlockHeight, facebook::yoga::SizingMode widthMode, facebook::yoga::Direction direction, facebook::yoga::LayoutData& layoutMarkerData, uint32_t depth, uint32_t generationCount); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 2f0733cb396..8ff381554a9 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -12678,7 +12678,7 @@ void facebook::yoga::assertFatal(bool condition, const char* message); void facebook::yoga::assertFatalWithConfig(const facebook::yoga::Config* config, bool condition, const char* message); void facebook::yoga::assertFatalWithNode(const facebook::yoga::Node* node, bool condition, const char* message); void facebook::yoga::calculateLayout(facebook::yoga::Node* node, float ownerWidth, float ownerHeight, facebook::yoga::Direction ownerDirection); -void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node *const node); +void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node* node, bool didPerformLayout); void facebook::yoga::constrainMaxSizeForMode(const facebook::yoga::Node* node, facebook::yoga::Direction direction, facebook::yoga::FlexDirection axis, float ownerAxisSize, float ownerWidth, facebook::yoga::SizingMode* mode, float* size); void facebook::yoga::fatalWithMessage(const char* message); void facebook::yoga::layoutAbsoluteChild(const facebook::yoga::Node* containingNode, const facebook::yoga::Node* node, facebook::yoga::Node* child, float containingBlockWidth, float containingBlockHeight, facebook::yoga::SizingMode widthMode, facebook::yoga::Direction direction, facebook::yoga::LayoutData& layoutMarkerData, uint32_t depth, uint32_t generationCount); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 62d88ff7546..659d05423e4 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -15021,7 +15021,7 @@ void facebook::yoga::assertFatal(bool condition, const char* message); void facebook::yoga::assertFatalWithConfig(const facebook::yoga::Config* config, bool condition, const char* message); void facebook::yoga::assertFatalWithNode(const facebook::yoga::Node* node, bool condition, const char* message); void facebook::yoga::calculateLayout(facebook::yoga::Node* node, float ownerWidth, float ownerHeight, facebook::yoga::Direction ownerDirection); -void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node *const node); +void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node* node, bool didPerformLayout); void facebook::yoga::constrainMaxSizeForMode(const facebook::yoga::Node* node, facebook::yoga::Direction direction, facebook::yoga::FlexDirection axis, float ownerAxisSize, float ownerWidth, facebook::yoga::SizingMode* mode, float* size); void facebook::yoga::fatalWithMessage(const char* message); void facebook::yoga::layoutAbsoluteChild(const facebook::yoga::Node* containingNode, const facebook::yoga::Node* node, facebook::yoga::Node* child, float containingBlockWidth, float containingBlockHeight, facebook::yoga::SizingMode widthMode, facebook::yoga::Direction direction, facebook::yoga::LayoutData& layoutMarkerData, uint32_t depth, uint32_t generationCount); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index b8feb8d7d21..8cabf8709cb 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -14887,7 +14887,7 @@ void facebook::yoga::assertFatal(bool condition, const char* message); void facebook::yoga::assertFatalWithConfig(const facebook::yoga::Config* config, bool condition, const char* message); void facebook::yoga::assertFatalWithNode(const facebook::yoga::Node* node, bool condition, const char* message); void facebook::yoga::calculateLayout(facebook::yoga::Node* node, float ownerWidth, float ownerHeight, facebook::yoga::Direction ownerDirection); -void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node *const node); +void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node* node, bool didPerformLayout); void facebook::yoga::constrainMaxSizeForMode(const facebook::yoga::Node* node, facebook::yoga::Direction direction, facebook::yoga::FlexDirection axis, float ownerAxisSize, float ownerWidth, facebook::yoga::SizingMode* mode, float* size); void facebook::yoga::fatalWithMessage(const char* message); void facebook::yoga::layoutAbsoluteChild(const facebook::yoga::Node* containingNode, const facebook::yoga::Node* node, facebook::yoga::Node* child, float containingBlockWidth, float containingBlockHeight, facebook::yoga::SizingMode widthMode, facebook::yoga::Direction direction, facebook::yoga::LayoutData& layoutMarkerData, uint32_t depth, uint32_t generationCount); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 5f8fc0c410b..425ba3f6239 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -9838,7 +9838,7 @@ void facebook::yoga::assertFatal(bool condition, const char* message); void facebook::yoga::assertFatalWithConfig(const facebook::yoga::Config* config, bool condition, const char* message); void facebook::yoga::assertFatalWithNode(const facebook::yoga::Node* node, bool condition, const char* message); void facebook::yoga::calculateLayout(facebook::yoga::Node* node, float ownerWidth, float ownerHeight, facebook::yoga::Direction ownerDirection); -void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node *const node); +void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node* node, bool didPerformLayout); void facebook::yoga::constrainMaxSizeForMode(const facebook::yoga::Node* node, facebook::yoga::Direction direction, facebook::yoga::FlexDirection axis, float ownerAxisSize, float ownerWidth, facebook::yoga::SizingMode* mode, float* size); void facebook::yoga::fatalWithMessage(const char* message); void facebook::yoga::layoutAbsoluteChild(const facebook::yoga::Node* containingNode, const facebook::yoga::Node* node, facebook::yoga::Node* child, float containingBlockWidth, float containingBlockHeight, facebook::yoga::SizingMode widthMode, facebook::yoga::Direction direction, facebook::yoga::LayoutData& layoutMarkerData, uint32_t depth, uint32_t generationCount); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 727e60d3be7..efbcee3bfbd 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -9829,7 +9829,7 @@ void facebook::yoga::assertFatal(bool condition, const char* message); void facebook::yoga::assertFatalWithConfig(const facebook::yoga::Config* config, bool condition, const char* message); void facebook::yoga::assertFatalWithNode(const facebook::yoga::Node* node, bool condition, const char* message); void facebook::yoga::calculateLayout(facebook::yoga::Node* node, float ownerWidth, float ownerHeight, facebook::yoga::Direction ownerDirection); -void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node *const node); +void facebook::yoga::cleanupContentsNodesRecursively(facebook::yoga::Node* node, bool didPerformLayout); void facebook::yoga::constrainMaxSizeForMode(const facebook::yoga::Node* node, facebook::yoga::Direction direction, facebook::yoga::FlexDirection axis, float ownerAxisSize, float ownerWidth, facebook::yoga::SizingMode* mode, float* size); void facebook::yoga::fatalWithMessage(const char* message); void facebook::yoga::layoutAbsoluteChild(const facebook::yoga::Node* containingNode, const facebook::yoga::Node* node, facebook::yoga::Node* child, float containingBlockWidth, float containingBlockHeight, facebook::yoga::SizingMode widthMode, facebook::yoga::Direction direction, facebook::yoga::LayoutData& layoutMarkerData, uint32_t depth, uint32_t generationCount);