From c316de1bdcc9985bb944d841495423595e374266 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Wed, 18 Mar 2026 18:44:34 -0700 Subject: [PATCH] Fix undefined behavior in LayoutAnimation sort comparator (#56127) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56127 `shouldFirstComeBeforeSecondMutation` in `utils.h` uses `return &lhs < &rhs` as a fallback when mutation types don't determine ordering. That's undefined behavior per C++ standard [expr.rel]/4, comparing pointers from different allocations (which happens during `std::stable_sort`'s merge phase). This wasn't a problem until the LLVM 19.x NDK upgrade changed libc++'s sort implementation. The new insertion sort phase is more aggressive about exploiting comparator correctness, and the UB now manifests as SIGSEGV in the insertion sort phase on Android. The fix replaces the pointer comparison with a deterministic fallback using `parentTag`, `newChildShadowView.tag`, and `oldChildShadowView.tag`. When all properties are equal, returns `false` to maintain stable ordering. Also adds `MutationComparatorTest.cpp` with tests covering: - Strict weak ordering properties (irreflexivity, asymmetry, transitivity) - Type-based ordering rules (deletes last, removes before inserts, etc.) - Deterministic fallback by parentTag, newChildTag, oldChildTag - Equal mutations return false (stability) - Stress test: `std::stable_sort` on large mutation lists with duplicates Changelog: [Internal] Reviewed By: christophpurrer Differential Revision: D95909371 --- .../tests/MutationComparatorTest.cpp | 273 ++++++++++++++++++ .../react/renderer/animations/utils.h | 13 +- 2 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/animations/tests/MutationComparatorTest.cpp diff --git a/packages/react-native/ReactCommon/react/renderer/animations/tests/MutationComparatorTest.cpp b/packages/react-native/ReactCommon/react/renderer/animations/tests/MutationComparatorTest.cpp new file mode 100644 index 000000000000..f5ed376a8859 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/animations/tests/MutationComparatorTest.cpp @@ -0,0 +1,273 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +#include + +#include +#include + +namespace facebook::react { + +namespace { + +ShadowView makeShadowView(Tag tag) { + ShadowView sv{}; + sv.tag = tag; + return sv; +} + +} // namespace + +// Verify strict weak ordering: irreflexivity +// comp(a, a) must be false +TEST(MutationComparatorTest, Irreflexivity) { + auto sv1 = makeShadowView(1); + auto sv2 = makeShadowView(2); + + // Same-type mutations where the fallback path is exercised + auto update1 = ShadowViewMutation::UpdateMutation(sv1, sv2, /*parentTag=*/10); + EXPECT_FALSE(shouldFirstComeBeforeSecondMutation(update1, update1)); + + auto create1 = ShadowViewMutation::CreateMutation(sv1); + EXPECT_FALSE(shouldFirstComeBeforeSecondMutation(create1, create1)); + + auto insert1 = + ShadowViewMutation::InsertMutation(/*parentTag=*/10, sv1, /*index=*/0); + EXPECT_FALSE(shouldFirstComeBeforeSecondMutation(insert1, insert1)); + + auto remove1 = + ShadowViewMutation::RemoveMutation(/*parentTag=*/10, sv1, /*index=*/0); + EXPECT_FALSE(shouldFirstComeBeforeSecondMutation(remove1, remove1)); + + auto del1 = ShadowViewMutation::DeleteMutation(sv1); + EXPECT_FALSE(shouldFirstComeBeforeSecondMutation(del1, del1)); +} + +// Verify strict weak ordering: asymmetry +// If comp(a, b) then !comp(b, a) +TEST(MutationComparatorTest, Asymmetry) { + auto sv1 = makeShadowView(1); + auto sv2 = makeShadowView(2); + auto sv3 = makeShadowView(3); + + // Two updates with same type but different parentTags (fallback path) + auto update1 = ShadowViewMutation::UpdateMutation(sv1, sv2, /*parentTag=*/10); + auto update2 = ShadowViewMutation::UpdateMutation(sv1, sv3, /*parentTag=*/20); + + bool a_before_b = shouldFirstComeBeforeSecondMutation(update1, update2); + bool b_before_a = shouldFirstComeBeforeSecondMutation(update2, update1); + + // Exactly one must be true (asymmetry) + EXPECT_NE(a_before_b, b_before_a); +} + +// Verify strict weak ordering: transitivity +// If comp(a, b) and comp(b, c) then comp(a, c) +TEST(MutationComparatorTest, Transitivity) { + auto sv1 = makeShadowView(1); + auto sv2 = makeShadowView(2); + auto sv3 = makeShadowView(3); + + auto update1 = ShadowViewMutation::UpdateMutation(sv1, sv1, /*parentTag=*/10); + auto update2 = ShadowViewMutation::UpdateMutation(sv1, sv1, /*parentTag=*/20); + auto update3 = ShadowViewMutation::UpdateMutation(sv1, sv1, /*parentTag=*/30); + + bool a_b = shouldFirstComeBeforeSecondMutation(update1, update2); + bool b_c = shouldFirstComeBeforeSecondMutation(update2, update3); + bool a_c = shouldFirstComeBeforeSecondMutation(update1, update3); + + if (a_b && b_c) { + EXPECT_TRUE(a_c) << "Transitivity violated: a