tauri-apps / tauri-apps/plugins-workspace

[updater] macOS: failed install can delete the user's app (backup lives in a self-deleting TempDir, no restore on failure)

Open
#3,505 0 comments 0 reactions 0 assignees View on GitHub
platform: macos plugin: updater type: bug
Dominant language
Rust
Stars
1.8k
Forks
602
Avg merge
4d 14h
Merged PRs (30d)
9

Description

### Describe the bug

On macOS, `install_inner` can **delete the user's installed app entirely** when the install fails partway: the backup of the current app is placed inside a `tempfile::TempDir`, and `TempDir`'s `Drop` deletes it on *every* exit path, including errors. There is no restore-on-failure, so a failed final rename leaves the user with no app at the install path, the backup destroyed, and the update not installed.

The Linux AppImage path already does this correctly - it renames the backup **back** on every failure branch ([updater.rs#L1018](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/updater/src/updater.rs#L1018), [#L1027](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/updater/src/updater.rs#L1027), [#L1037](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/updater/src/updater.rs#L1037)). The macOS path has no equivalent.

### Failure sequence (current `v2` HEAD)

All references are to [`plugins/updater/src/updater.rs`](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/updater/src/updater.rs):

1. Backup and extraction dirs are `TempDir`s in `$TMPDIR` ([L1224-L1230](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/updater/src/updater.rs#L1224-L1230)).
2. The current `.app` is **renamed into the backup TempDir** ([L1255-L1258](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/updater/src/updater.rs#L1255-L1258)). From this moment the only copy of the user's app lives in a directory that deletes itself on drop.
3. The new app is renamed into place ([L1302](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/updater/src/updater.rs#L1302)). If this rename fails, the `?` propagates, the function returns, **both `TempDir`s drop, and the backup is deleted**. Result: no app at the target path, old app gone, update not installed.
4. Independent of the failure case, there is a **no-app window** between steps 2 and 3 - a crash or power loss there leaves both bundles in `$TMPDIR` (which macOS purges periodically) and nothing at the install path.

A concrete, reproducible trigger for step 3 failing: `$TMPDIR` on a different volume than the app. `std::fs::rename` fails with `EXDEV` (cross-device link), which is not a `PermissionDenied`, so the AppleScript branch doesn't catch it either.

### Reproduction

App installed on an external/secondary APFS volume (or any setup where `$TMPDIR` and the app are on different filesystems):

```
TMPDIR=/Volumes/OtherDisk/tmp /Applications/MyApp.app/Contents/MacOS/myapp
# trigger an update; download + signature verification succeed
# install: rename(old -> $TMPDIR backup) succeeds only if same volume; if the
# backup rename succeeds but the final rename hits EXDEV (or any transient
# error), install_inner returns Err and the TempDir Drop deletes the backup
```

After the failed install: `/Applications/MyApp.app` does not exist and the previous version is unrecoverable.

### Related hardening in the same function

The privileged fallback ([L1270-L1295](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/updater/src/updater.rs#L1270-L1295)) is `rm -rf '{src}' && mv -f '{new}' '{src}'` via AppleScript - the original app is destroyed **before** the replacement lands (and in this branch the backup rename already failed, so there is no backup at all). A cancelled auth prompt mid-script, or an `mv` failure after `rm -rf` completes, also leaves no app. The paths are also single-quote-interpolated into a shell string, so a path containing `'` breaks the quoting.

### Suggested fix

1. Keep the backup in a non-self-deleting location (e.g. `TempDir::keep()`/`into_path()`), or at minimum only let it drop after the final rename succeeded.
2. On final-rename failure, rename the backup back into place before returning the error - mirroring what the AppImage path already does.
3. Optionally, on macOS the whole two-rename dance can be replaced by one atomic `renamex_np(new, old, RENAME_SWAP)` (what Sparkle uses, weak-imported), which eliminates both the no-app window and the need for ordering, with the current logic as fallback for filesystems without swap support.
4. In the AppleScript branch, `mv` the old app aside instead of `rm -rf`, and delete it only after the replacement is in place.

Happy to open a PR for 1+2 (and optionally 3) if maintainers agree with the direction.

### Platform and versions

- plugin: `tauri-plugin-updater` 2.10.0 (code unchanged at current `v2` HEAD)
- macOS (any); analysis done on macOS 27.0, tauri 2.11.2

Contributor guide

Open the contributing guide

Research direction

Read plugins/updater/src/updater.rs around lines 1224-1302, then compare the Linux AppImage failure branches around lines 1018, 1027, and 1037. Preserve the old macOS app when installation fails and verify that failed or interrupted replacement attempts do not leave the install path empty; the payload does not mention a specific test file.

Written by the indexing model from the issue text.

Assessment

Tech stack
macos, rust, tauri
Domain
desktop, release
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.