Fix: make iOS bundle extraction atomic and self-healing#153
Open
voicecode-bv wants to merge 1 commit into
Open
Fix: make iOS bundle extraction atomic and self-healing#153voicecode-bv wants to merge 1 commit into
voicecode-bv wants to merge 1 commit into
Conversation
On first launch (and bundle updates) the app unzips app.zip into Documents/app via extractZipParallel(). Two issues there can leave a permanently broken install that crashes on a missing `require` (e.g. "Failed to open stream: .../symfony/deprecation-contracts/function.php"): 1. Phase 3 writes files on a concurrent queue AND each writer calls createDirectory(withIntermediateDirectories:) for its own parent. Many files share parent dirs (vendor/symfony/...), so writers race to create the same intermediate directories - a TOCTOU race on APFS that can spuriously fail and abort extraction. The error is caught in copyBundledApp() but the half-written app (often including .env) is left on disk, and installed.version is never written. 2. hasApp() keys readiness off .env existing. After a partial extraction .env may be present while other files are missing, so the next launch treats the broken install as complete and never re-extracts -> crashes forever until reinstall. Reproduces intermittently depending on device timing/memory pressure. Fixes: - Pre-create all unique parent directories sequentially (Phase 2); Phase 3 writers now only write leaf files, removing the directory race. - Verify every expected file exists after writing (Phase 4); a missing file throws instead of silently producing a partial install. - copyBundledApp() removes the partial app dir on failure, so the next launch re-extracts cleanly (self-healing). - hasApp() now keys off installed.version (written only after a fully successful extraction) instead of .env.
simonhamp
approved these changes
Jun 4, 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.
Problem
On first launch (and on bundle updates) the iOS app unzips
app.zipintoDocuments/appviaAppUpdateManager.extractZipParallel(). Under certain device timing / memory conditions this can leave a permanently broken install that crashes on a missingrequireon every launch, e.g.:The file is present in the bundle but missing on the device. Seen on App Store / TestFlight installs, intermittently.
Root cause
Directory creation race. Phase 3 writes files on a
.concurrentDispatchQueue, and every writer also callscreateDirectory(withIntermediateDirectories: true)for its own parent. Many files share parent directories (vendor/symfony/...), so concurrent writers race to create the same intermediate directories — a TOCTOU race on APFS that can spuriously fail and abort the whole extraction. The error is caught incopyBundledApp(), but the half-written app (often already including.env) is left on disk andinstalled.versionis never written.Readiness keyed off
.env.hasApp()returnstrueas soon as.envexists. After a partial extraction.envmay be present while other files are missing, so the next launch treats the broken install as complete and never re-extracts → crashes forever until reinstall.Fix
Set), so Phase 3 writers only touch leaf files — the directory race is gone.copyBundledApp(): on any extraction failure, remove the partial app directory so the next launch re-extracts cleanly (self-healing).hasApp(): key readiness offinstalled.version(written only after a fully successful extraction) instead of.env.Net effect: extraction is now all-or-nothing. A failed/interrupted extract self-heals on the next launch instead of poisoning the install — including installs already broken by this bug in the field, which recover on the next app update.
Possible follow-up (not in this PR)
Phase 1 still buffers every file's bytes in memory simultaneously (
fileDataMap), a jetsam risk on large vendor dirs / low-memory devices. Left out to keep the diff focused; with this change such a failure now self-heals rather than persisting, but bounding peak memory (stream/chunk writes) would be a worthwhile separate improvement.Testing
Verified by inspection; the changed paths are guarded by existing
print/throwdiagnostics. There is no automated coverage for the native extraction path in the repo.