Skip to content

[Pro 1.8.1 / iOS] setTheme deadlocks on cold start: JS thread dispatch_syncs the main queue while the main thread waits on JS #751

Closed

Description

@ryzencool

What happened?

Uniwind.setTheme on iOS can deadlock the process during launch. The system then kills it with the scene-update watchdog (0x8badf00d).

This is a different path from #745. #745 is the prepareTransition snapshot: it blocks the JS thread, but returns once the main thread is free. This path never returns.

Call path (Pro 1.8.1): Uniwind.setTheme → HybridUniwindConfig::setTheme → NativeIOSPlatform.setTheme → onNativePlatformChange(source: .user) → getLatest(). getLatest() reads UIKit through onMainThread:

func onMainThread<T>(_ closure: @escaping () -> T) -> T {
    if Thread.isMainThread {
        return closure()
    } else {
        return DispatchQueue.main.sync(execute: closure)
    }
}

setTheme runs on the JS thread, so this is DispatchQueue.main.sync from the JS thread.

React Native's iOS event beat drains the JS queue from the main run loop with executeSynchronouslyOnSameThread_CAN_DEADLOCK: the main thread blocks until the JS runtime finishes the current work. When that work is a setTheme that syncs back to the main queue, each side waits for the other.

Measured on a release build, iPhone 17, iOS 27, Uniwind Pro 1.8.1. The launch screen stayed up for about two minutes, then the process disappeared. The same device had four more kills the same afternoon, plus the same signature on an earlier build that morning. During the hang the app's own CPU time was 0.014s, so it was blocked, not busy. These reports stayed on device (share_with_app_devs = 0), so they do not show up in App Store Connect.

The two stacks line up:

  • Main thread: run-loop event callback → executeSynchronouslyOnSameThread_CAN_DEADLOCK, waiting for the JS runtime.
  • JS thread: Uniwind.setTheme → NativeIOSPlatform.setTheme → dispatch_sync onto the main queue.

It only happens when the theme call lands inside a main-thread event beat, so it takes several cold launches to hit. The splash never hides because the root layout never commits.

prepareTransition adds a second onMainThread hop on the same call (the snapshot from #745). Either hop is enough to deadlock; both run on every non-animated setTheme that changes the effective theme.

Suggested fix: never DispatchQueue.main.sync from the JS thread.

Steps to Reproduce

  1. Release build on iOS, Pro 1.8.1.
  2. Call Uniwind.setTheme('dark') with no transition preset from JS while the app is still bootstrapping, for example from a module initializer or the first layout effect, before the first screen commits.
  3. Relaunch until the call overlaps the main-thread event beat. The launch screen then stays up until the watchdog kills the process (about 2 minutes).

Snack or Repository Link (Optional)

No response

Uniwind version

1.8.1 (Pro)

React Native Version

0.88

Platforms

iOS

Expo

Yes (SDK 58)

Additional information

Related: #745 covers the wasted snapshot on the same call. Fixing only the snapshot still leaves onNativePlatformChange → getLatest() → DispatchQueue.main.sync, which deadlocks on its own.

Activity

  1. added
    pending-releaseFixed, waiting for new version to be released
    and removed on Oct 26, 2026
  2. github-actions commented on Oct 28, 2026

    @github-actions
    Contributor

    New release is published: https://docs.uniwind.dev/pro/changelog

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions