Skip to content

fix(LayoutAnimations): deadlock in ReanimatedCommitHook surface initialization#9903

Draft
bartlomiejbloniarz wants to merge 1 commit into
mainfrom
fix-shadow-tree-registry-deadlock
Draft

fix(LayoutAnimations): deadlock in ReanimatedCommitHook surface initialization#9903
bartlomiejbloniarz wants to merge 1 commit into
mainfrom
fix-shadow-tree-registry-deadlock

Conversation

@bartlomiejbloniarz

Copy link
Copy Markdown
Contributor

Summary

Fixes #8579.

maybeInitializeLayoutAnimations enumerated the ShadowTreeRegistry from inside a commit hook. Commits usually execute inside ShadowTreeRegistry::visit, which already holds the registry's shared_mutex in shared mode, so the enumerate was a recursive shared acquisition. libc++ writers block new readers while draining existing ones, so a concurrent startSurface/stopSurface (a registry writer) on another thread turns that recursion into a permanent deadlock — the three-thread ANR in the issue (JS in enumerate, pool thread in startSurface, main in constraintLayout).

The registry is now read exactly once, at hook construction (module init — never a commit stack), to pick up surfaces that existed before Reanimated loaded (lazy init, brownfield). After that, the first commit of each new surface registers the mounting override delegate directly through the committing ShadowTree — no registry access on commit stacks at all. currentMaxSurfaceId_ becomes a seenSurfaces_ set, and a mount hook erases stopped surfaces from it. That last part is not hypothetical: a JS reload reuses the same surface id, so without the erase the recreated surface would never get the override delegate.

Test plan

fabric-example, iOS sim + Android emulator:

  • Layout animations run on the main surface (delegate registered via the commit-hook path) and on a LogBox surface created after init (the case a registry-free fix has to cover)
  • 5×/10× reload churn (surface stop + start + first commits racing) with layout animations between reloads: responsive throughout, zero ANRs, stable pid
  • Back-button surface stop + relaunch on Android, empty-root commits (render-null) — all clean
  • The emulator still had ANR traces from the unfixed binary with the 【Android ANR】RN 0.81.5 RCT_NEW_ARCH_ENABLED = true ReanimatedCommitHook::maybeInitializeLayoutAnimations #8579 signature; the fixed binary produced none under the same and heavier churn

…alization

maybeInitializeLayoutAnimations enumerated the ShadowTreeRegistry from
inside a commit hook. Commits usually execute inside
ShadowTreeRegistry::visit, which already holds the registry's
shared_mutex in shared mode, so the enumerate was a recursive shared
acquisition. libc++ writers block new readers while draining existing
ones, so a concurrent startSurface/stopSurface (a registry writer) on
another thread turns that recursion into a permanent deadlock - reported
as production ANRs in #8579.

The registry is now read exactly once, at hook construction (module
init, never a commit stack), to pick up surfaces that existed before
Reanimated loaded. After that, the first commit of each new surface
registers the mounting override delegate directly through the committing
ShadowTree - no registry access on commit stacks at all.
currentMaxSurfaceId_ becomes a seenSurfaces_ set, and a mount hook
erases stopped surfaces from it so surface id reuse cannot leave a new
surface unregistered.

Fixes #8579
@bartlomiejbloniarz bartlomiejbloniarz force-pushed the fix-shadow-tree-registry-deadlock branch from c7685da to 5d9d63f Compare July 8, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

【Android ANR】RN 0.81.5 RCT_NEW_ARCH_ENABLED = true ReanimatedCommitHook::maybeInitializeLayoutAnimations

1 participant