Skip to content

Commit e681736

Browse files
erickreutzmeta-codesync[bot]
authored andcommitted
Fix Android accessibility descendant race in ReactViewGroup (#56182)
Summary: Guard `ReactViewGroup.addChildrenForAccessibility` against transient non-descendant races during accessibility traversal on Android. This replaces direct `super.addChildrenForAccessibility(...)` calls with a small wrapper that: - catches `IllegalArgumentException` - only swallows the specific "descendant of this view" case - rethrows all other `IllegalArgumentException`s ## Why There are recurring crashes with stack traces ending in: `ViewGroup.offsetRectBetweenParentAndChild` -> `offsetDescendantRectToMyCoords` -> `addChildrenForAccessibility` -> `IllegalArgumentException: parameter must be a descendant of this view`. This can happen when accessibility is traversing children while views are being re-parented/removed. Related reports: - #32649 - #38925 - #7377 - kirillzyusko/react-native-keyboard-controller#961 - kirillzyusko/react-native-keyboard-controller#962 - dotnet/maui#32927 There is also precedent in RN for handling this class of non-descendant race defensively: - #55273 ## Scope This is intentionally minimal and localized to accessibility child collection in `ReactViewGroup`. Behavior is unchanged in the non-racy path. ## Changelog: [Android] [Fixed] - Guard `ReactViewGroup.addChildrenForAccessibility` against transient non-descendant accessibility traversal crashes. Pull Request resolved: #56182 Test Plan: Validated in a downstream RN Android app on Android 16 with repeated sheet/modal open-close stress and active accessibility hierarchy traversal; crash reproduces before patch and no longer reproduces with this guard. Reviewed By: jorge-cab Differential Revision: D97861147 Pulled By: Abbondanzo fbshipit-source-id: 15e624a94d5acd99777a7426787b52d8e85cdf8a
1 parent 8915341 commit e681736

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactSoftExceptionLogger.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.annotation.StringDef
1111
import com.facebook.common.logging.FLog
1212
import com.facebook.proguard.annotations.DoNotStrip
1313
import com.facebook.react.bridge.ReactSoftExceptionLogger.Categories.CLIPPING_PROHIBITED_VIEW
14+
import com.facebook.react.bridge.ReactSoftExceptionLogger.Categories.RVG_ADD_CHILDREN_FOR_ACCESSIBILITY
1415
import com.facebook.react.bridge.ReactSoftExceptionLogger.Categories.RVG_IS_VIEW_CLIPPED
1516
import com.facebook.react.bridge.ReactSoftExceptionLogger.Categories.RVG_ON_VIEW_REMOVED
1617
import com.facebook.react.bridge.ReactSoftExceptionLogger.Categories.SOFT_ASSERTIONS
@@ -21,6 +22,7 @@ import java.util.concurrent.CopyOnWriteArrayList
2122
internal object ReactSoftExceptionLogger {
2223
@Retention(AnnotationRetention.SOURCE)
2324
@StringDef(
25+
RVG_ADD_CHILDREN_FOR_ACCESSIBILITY,
2426
RVG_IS_VIEW_CLIPPED,
2527
RVG_ON_VIEW_REMOVED,
2628
CLIPPING_PROHIBITED_VIEW,
@@ -31,6 +33,8 @@ internal object ReactSoftExceptionLogger {
3133

3234
/** Constants that listeners can utilize for custom category-based behavior. */
3335
object Categories {
36+
const val RVG_ADD_CHILDREN_FOR_ACCESSIBILITY: String =
37+
"ReactViewGroup.addChildrenForAccessibility"
3438
const val RVG_IS_VIEW_CLIPPED: String = "ReactViewGroup.isViewClipped"
3539
const val RVG_ON_VIEW_REMOVED: String = "ReactViewGroup.onViewRemoved"
3640
const val CLIPPING_PROHIBITED_VIEW: String = "ReactClippingProhibitedView"

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -997,12 +997,12 @@ public open class ReactViewGroup public constructor(context: Context?) :
997997
} else if (axOrderParentOrderList != null) {
998998
// view is a container so add its children normally
999999
if (!isFocusable) {
1000-
super.addChildrenForAccessibility(outChildren)
1000+
safeAddChildrenForAccessibility(outChildren)
10011001
return
10021002

10031003
// If this view can coopt, turn the focusability off its children but add them to the tree
10041004
} else if (isFocusable && (contentDescription == null || contentDescription == "")) {
1005-
super.addChildrenForAccessibility(outChildren)
1005+
safeAddChildrenForAccessibility(outChildren)
10061006
for (i in 0..<childCount) {
10071007
ReactAxOrderHelper.disableFocusForSubtree(getChildAt(i), axOrderParentOrderList)
10081008
}
@@ -1012,7 +1012,23 @@ public open class ReactViewGroup public constructor(context: Context?) :
10121012
return
10131013
}
10141014
} else {
1015+
safeAddChildrenForAccessibility(outChildren)
1016+
}
1017+
}
1018+
1019+
private fun safeAddChildrenForAccessibility(outChildren: ArrayList<View>) {
1020+
try {
10151021
super.addChildrenForAccessibility(outChildren)
1022+
} catch (error: IllegalArgumentException) {
1023+
// Android 16 can race while building accessibility child lists during fast re-parenting.
1024+
if (error.message?.contains("descendant of this view") == true) {
1025+
logSoftException(
1026+
ReactSoftExceptionLogger.Categories.RVG_ADD_CHILDREN_FOR_ACCESSIBILITY,
1027+
error,
1028+
)
1029+
} else {
1030+
throw error
1031+
}
10161032
}
10171033
}
10181034

0 commit comments

Comments
 (0)