From b2915ced0f21d4df7d2729cf6e0bfb11f0100627 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 8 Jun 2026 06:32:42 -0700 Subject: [PATCH] Fix `display: contents` nodes having `hasNewLayout` set incorrectly (#57103) Summary: 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 --- .../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/ReactAndroidNewarchCxx.api | 2 +- .../api-snapshots/ReactAndroidReleaseCxx.api | 2 +- .../api-snapshots/ReactAppleDebugCxx.api | 2 +- .../api-snapshots/ReactAppleNewarchCxx.api | 2 +- .../api-snapshots/ReactAppleReleaseCxx.api | 2 +- .../api-snapshots/ReactCommonDebugCxx.api | 2 +- .../api-snapshots/ReactCommonNewarchCxx.api | 2 +- .../api-snapshots/ReactCommonReleaseCxx.api | 2 +- 12 files changed, 23 insertions(+), 18 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 f48f261e31f..1db7a5c21af 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/CalculateLayout.cpp @@ -516,7 +516,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()) { @@ -524,11 +526,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); } } } @@ -1617,7 +1621,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; } @@ -1635,7 +1639,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; } @@ -1653,7 +1657,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; } @@ -1665,7 +1669,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 cfe0cb56238..c44d48b6252 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -12934,7 +12934,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/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 3432343ed07..dd01b050910 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -12572,7 +12572,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 d46afaf86d4..c56d1e02cff 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -12787,7 +12787,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 34fb35faf45..cc1a910e081 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -14748,7 +14748,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/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 7f341310bcb..b46962198fa 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -14448,7 +14448,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 b20a27166d5..1fd3e340c7d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -14611,7 +14611,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 c3586f5df4c..6f3564afb73 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -9899,7 +9899,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/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 70992106bd8..8f00ecd9e8d 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -9739,7 +9739,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 18bb4baca12..ccc712d9617 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -9890,7 +9890,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);