Skip to content

Commit d2923af

Browse files
committed
Bugfix: Fix crash on launch after auto-update
The new, custom auto-updater was absolutely broken! Luckily, the fix was simple: - `fs::copy` overwrites files in-place, keeping the same inode. macOS's kernel code signing cache is keyed by inode, so it validates the new binary's pages against the old binary's cached code directory → `SIGKILL (Code Signature Invalid)` before any app code runs. - Fix: write to a temp file, then `rename()` into place. This creates a new inode, forcing the kernel to validate the code signature fresh. - The admin-privilege path (`rsync -a`) already uses atomic rename by default, so only the direct-write path was affected.
1 parent d0746fb commit d2923af

2 files changed

Lines changed: 20 additions & 1 deletion

File tree

apps/desktop/src-tauri/src/updater/CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ signatures use base64(minisign-text-format) encoding, matching Tauri's internal
3838
`do shell script ... with administrator privileges` shows the native macOS auth dialog. `rsync` is used because it
3939
expresses the full sync (copy + delete stale) in a single shell command.
4040

41+
**Decision**: Atomic rename (write to temp file, then `rename()`) instead of in-place `fs::copy`.
42+
**Why**: `fs::copy` overwrites the destination in-place, keeping the same inode. macOS's kernel code signing cache
43+
keys on inode — it validates the new binary's pages against the old binary's cached code directory, causing
44+
`SIGKILL (Code Signature Invalid)` on launch. Atomic rename creates a new inode, forcing a fresh validation.
45+
The admin-privilege path (`rsync -a`) already uses atomic rename by default.
46+
4147
## Key patterns and gotchas
4248

4349
- **macOS-only.** The module, command registrations, and `UpdateState` are all gated with `#[cfg(target_os = "macos")]`.

apps/desktop/src-tauri/src/updater/installer.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,26 @@ fn sync_remaining(src_root: &Path, dest_root: &Path, all_src_paths: &HashSet<Pat
165165
}
166166

167167
/// Copies a file, creating parent directories as needed.
168+
///
169+
/// Uses atomic rename (write to temp file, then rename) instead of in-place overwrite.
170+
/// This creates a new inode, which prevents macOS's kernel code signing cache from
171+
/// validating the new binary's pages against the old binary's cached code directory.
168172
fn copy_file_creating_dirs(src: &Path, dest: &Path) -> Result<(), String> {
169173
if let Some(parent) = dest.parent()
170174
&& !parent.exists()
171175
{
172176
fs::create_dir_all(parent).map_err(|e| format!("Couldn't create dir {}: {e}", parent.display()))?;
173177
}
174-
fs::copy(src, dest).map_err(|e| format!("Couldn't copy {} -> {}: {e}", src.display(), dest.display()))?;
178+
179+
// Write to a temp file in the same directory, then atomically rename.
180+
// This ensures the destination gets a new inode, avoiding stale kernel code signing cache.
181+
let temp = dest.with_extension("cmdr-update-tmp");
182+
fs::copy(src, &temp).map_err(|e| format!("Couldn't copy {} -> {}: {e}", src.display(), temp.display()))?;
183+
fs::rename(&temp, dest).map_err(|e| {
184+
// Clean up temp file on rename failure
185+
let _ = fs::remove_file(&temp);
186+
format!("Couldn't rename {} -> {}: {e}", temp.display(), dest.display())
187+
})?;
175188
Ok(())
176189
}
177190

0 commit comments

Comments
 (0)