From 06e37a30a265fcd1b34f37a798d2c72167a6f165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20=C5=BBelawski?= Date: Tue, 25 Aug 2026 13:12:23 +0200 Subject: [PATCH] Skip non-owned subtrees in roundLayoutResultsToPixelGrid roundLayoutResultsToPixelGrid recurses into every yoga child and writes rounded positions and dimensions back into each node. Unlike the layout pass, which clones children before mutating them (cloneChildrenIfNeeded), the rounding pass crosses the ownership frontier into subtrees that are structurally shared with other shadow trees. Under Fabric, two trees can run layout concurrently: commits run layoutIfNeeded on candidate trees before taking the commit mutex, and concurrent committers (React on the JS thread and a library committing from another thread) share every unchanged subtree. Both rounding passes then mutate the same shared yoga nodes. ThreadSanitizer reports this as a data race: reads at PixelGrid.cpp:72/75 against writes at PixelGrid.cpp:89/109 via Node::setLayoutPosition. Skipping children whose owner is not the current node is safe: - YogaLayoutableShadowNode::layout only copies metrics from children with hasNewLayout, and asserts those children are owned (YGNodeGetOwner(childYogaNode) == &yogaNode_). - hasNewLayout is only set on nodes the pass performed layout on, so nodes past the ownership frontier are cache-restored, keep the flag unset, and the metrics-copying recursion never descends into them. - The shadow nodes past the frontier were not cloned in this commit, so they are sealed and cannot accept new LayoutMetrics at all. Rounded values written to non-owned nodes therefore have no reader; the writes can only corrupt the state of other trees. The guard mirrors the existing owner checks in Node::cloneChildrenIfNeeded and YGNodeFreeRecursive ("Don't free shared nodes that we don't own"). Observed in react-native-reanimated's ThreadSanitizer nightly CI: https://github.com/software-mansion/react-native-reanimated/actions/runs/32811464205/job/97691453444 --- .../react-native/ReactCommon/yoga/yoga/algorithm/PixelGrid.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-native/ReactCommon/yoga/yoga/algorithm/PixelGrid.cpp b/packages/react-native/ReactCommon/yoga/yoga/algorithm/PixelGrid.cpp index 61de2be2e8f6..5c5a3b7ffbaf 100644 --- a/packages/react-native/ReactCommon/yoga/yoga/algorithm/PixelGrid.cpp +++ b/packages/react-native/ReactCommon/yoga/yoga/algorithm/PixelGrid.cpp @@ -128,6 +128,9 @@ void roundLayoutResultsToPixelGrid( } for (yoga::Node* child : node->getChildren()) { + if (child->getOwner() != node) { + continue; + } roundLayoutResultsToPixelGrid(child, absoluteNodeLeft, absoluteNodeTop); } }