Skip to content

fix(updater): restore the current app when a macOS install fails (fix #3505, #3506) - #3578

Open
alii13 wants to merge 3 commits into
tauri-apps:v2from
alii13:fix/updater-macos-restore-on-failure
Open

alii13 wants to merge 3 commits into
tauri-apps:v2from
alii13:fix/updater-macos-restore-on-failure

Conversation

@alii13

@alii13 alii13 commented Sep 10, 2026

Copy link
Copy Markdown

Fixes #3505, fixes #3506.

I hit this on a Mac where the update kept "failing" and afterwards the app was simply gone from /Applications. The cause is in the macOS install_inner: the current .app gets renamed into a TempDir before the new bundle is renamed into place, and if that second rename fails the function just returns the error. Nothing puts the old bundle back, and the TempDir drop deletes it on the way out. The admin fallback has the same problem in shell form, it does rm -rf on the app first and mv the new one in second.

While in there I also fixed the 0700 bundle root from #3506, since it comes from the same temp dir.

What changed:

  • on APFS the current and new bundles are exchanged in one step with renamex_np(RENAME_SWAP) (what Sparkle does), so there is no moment where the install path is empty, not even if the process dies mid-update
  • where the file system can't swap (ENOTSUP), the two renames go through replace_bundle(), which renames the backup back if the second rename fails
  • the AppleScript now moves the old app aside and only removes it once the new one is in place; if the second mv fails it moves the old one back
  • compare st_dev of the app and the temp dir before touching anything and return the existing TempDirNotOnSameMountPoint (same as the AppImage path already does), instead of failing after the app has been moved
  • chmod the staged bundle root to 0755

The only dependency change is libc for the macOS target, which was already in the lockfile through tauri.

Testing: three unit tests, swap_bundle exchanging two directories (macOS only, skips on a file system that reports ENOTSUP), and replace_bundle on the normal path and on the restore when the second rename fails. cargo test, clippy -D warnings and fmt --check with the flags CI uses are clean on macOS. Side note, the two doctests in lib.rs already fail on v2 before this change; --all-targets skips doctests so CI never sees them.

I also ran it for real in an app pointed at this branch through [patch.crates-io], serving a signed update from localhost. A normal update swaps the bundle in a few seconds and it ends up 0755 with nothing left in the temp dir. Killing the app with kill -9 two seconds in (mid download) leaves the installed bundle untouched and it updates fine on the next launch; killing it a bit later just lands after the swap. In none of the runs did the install path end up empty, which was the whole problem.

On macOS `install_inner` moved the installed app into a `TempDir` backup and
then renamed the new bundle into place. Neither of the two steps after that
move restored the app on failure, and the `TempDir` deleted the backup on
every exit path, so a failed final rename left the user with no app at the
install path and no way back. The privileged fallback had the same shape:
`rm -rf` the app, then `mv` the new one in.

Both moves are now `replace_bundle`, which renames the current app to the
backup, renames the staged bundle into place, and renames the backup back if
that second step fails. The privileged script moves the current app aside,
moves the new one in, and restores on failure; it deletes the previous bundle
only after the new one is in place. The temp and install locations are
compared with `st_dev` before anything moves, returning the existing
`TempDirNotOnSameMountPoint` rather than failing after the app has already
been moved, and the staged bundle root is set to 0755 because `tempfile`
creates it 0700 and that mode followed it into `/Applications`.

Two unit tests cover `replace_bundle`: the staged bundle ends up in place with
the previous one in the backup, and a failed second rename leaves the current
bundle where it was and returns that error.

Closes tauri-apps#3505, closes tauri-apps#3506.
@alii13
alii13 marked this pull request as ready for review September 10, 2026 15:27
@alii13
alii13 requested a review from a team as a code owner September 10, 2026 15:27
Even with the restore in place there was a moment between the two renames
where the install path held nothing, and a process killed in that moment
left the previous app in the temp dir rather than where it belonged.

`swap_bundle` calls `renamex_np` with `RENAME_SWAP`, which exchanges the two
directories in one step so the install path always holds one of the two. The
previous app ends up in the extraction temp dir and is removed with it. Where
the file system does not support the swap (`ENOTSUP`, or `EINVAL` on older
systems) the two-rename path with the restore is used as before, and a
`PermissionDenied` from either still goes to the privileged fallback.

`libc` is added for the macOS target only; it was already in the lockfile
through tauri. One macOS test checks the exchange and skips on a file system
that reports `ENOTSUP`.
@alii13

alii13 commented Sep 20, 2026

Copy link
Copy Markdown
Author

@Legend-Master can you please review this PR? Thank you!

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

Projects

None yet

2 participants