iOS: roll back stuck pending updates before JS starts, fix crash - #27
Merged
Conversation
ofalvai
force-pushed
the
push-pzqxyqkskoyo
branch
from
August 10, 2026 14:03
1cb6d90 to
15ded75
Compare
Moves the rollback decision for a stuck (isLoading==YES) pending update from -initializeUpdateAfterRestart (runs mid bundle-eval, after JS/Fabric already exist) into +bundleURLForResource:..., the bridge's source-URL provider, which runs before any JS or Fabric surface exists. This removes the in-process reload that raced RN 0.86's Fabric surface teardown/recreate internals and crashed CI intermittently on localPackage.install.revert.dorevert (EXC_BAD_ACCESS / SIGSEGV). The rollback itself (+[CodePushPackage rollbackPackage] + bookkeeping) is extracted into a new +[CodePush rollbackPendingUpdate] class method, callable with no bridge/instance around. The old -rollbackPackage instance method (which called -loadBundle to trigger the racy reload) is now dead and removed; -saveFailedUpdate: became a class method since it is shared by both the download-failure path and the new rollback path. Note: the IMMEDIATE-install/restartApp() reload path has the same underlying Fabric race and is not addressed here; that needs a separate follow-up.
ofalvai
force-pushed
the
push-pzqxyqkskoyo
branch
from
August 10, 2026 15:59
15ded75 to
b159320
Compare
ofalvai
marked this pull request as ready for review
August 11, 2026 10:48
miklosboros
approved these changes
Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
At the moment, the decision to roll back a bad update happens on the JS side, therefore, Hermes and JS code are already running at the point when the rollback happens. A rollback involves
RCTTriggerReloadCommandListenersand a teardown of the JS runtime instance. This teardown could race with the initialization of the new instance, leading to a concrete crash. We can reproduce this crash even in CI today.Much more detailed LLM summary
Captured from a real iOS Simulator
.ipsreport (TestCodePush-2026-08-06-113918.ips) generated duringlocalPackage.install.revert.dorevert.EXC_BAD_ACCESS (SIGSEGV),KERN_INVALID_ADDRESS at 0x0000000000000008.com.apple.root.user-interactive-qos):com.facebook.react.runtime.JavaScript) was mid-teardown of the previous instance:A new Fabric surface was starting up while the old
ReactInstancewas concurrently being destructed — a teardown/recreate race in Fabric's mounting layer, hit exactly during the reload CodePush triggers to apply an update.CodePush.jsaccessesNativeModules.CodePushat import time, so the native module's-initruns during initial bundle evaluation:On the rollback path specifically, this schedules the reload before the new surface's
start()has even been queued (surface start happens via the buffered runtime executor, which only runs once bundle evaluation finishes) — so the race isn't bad luck on this path, it's structurally near-guaranteed to attempt an overlap.localPackage.install.revert.dorevert's third launch (test/template/scenarios/scenarioInstallWithRevert.jsinstalls a bundle that never callsnotifyApplicationReady, forcing a rollback on the next launch — seetest/test.ts:963-994) exercises exactly this path.Android's own
CodePush.javacallsinitializeUpdateAfterRestart()from the module's constructor, invoked fromMainApplication.getPackages()— i.e. before any JS exists. The bundle-file decision is made and settled before React ever starts, so there's no analogous "reload to correct a decision already made mid-bundle-eval." iOS is the outlier here.What
Align the iOS rollback mechanism with Android: the rollback decision is made on the native side before any JS is initialized.
Concrete changes:
CodePush.bundleURLForResource()(the main entry point that decides which bundle to load) is now performing the rollback and returns the rolled back bundle URL.initializeUpdateAfterRestart) no longer performs rollbacks, it is now only responsible for "bookkeeping" of pending update state.rollbackPackage()a more precise name (discardStuckPendingUpdate()) as it no longer hot-reloads a new bundle, it just "swaps" the active package so that the next launch loads the right one.saveFailedUpdate()can be a class method as it doesn't touch instance state (this whole class is already messy enough, let's prevent future mistakes by limiting who has access to what)Decisions
IMMEDIATE/restartApp() usage: not covered by this PR, that codepath also has a race condition and leads to a different runtime crash.
notifyApplicationReady() JS API: for users who manually mark an update as working, nothing changes. The rollback handling that this PR touches is based on
PendingUpdateIsLoadingKey, which is written by the previous app session (before a crash). So the rollback decision andnotifyApplicationReady()call happen in different launches. Also, the Android side already handles rollbacks this way, we are just aligning the iOS side to that.Bigger refactors of
CodePush.m: I resisted the urge to pull apart that large file or introduce Swift or anything like that. I think we should still do these, and I have a few draft prototypes, but those should land as different PRs.