Skip to content

Commit 456a94f

Browse files
authored
fix(macOS): updater EXC_BAD_ACCESS (#2181)
* fix(ci): updater artifacts * add temporary macos signature * add entitlement and notarization credentials * WIP macos fix * build version 2.0.0 with macos signature * [ci skip] revert version to `1.0.0` * sandbox current app to a directory * make clippy happy * [ci skip] disable `Notarization` in CI tests * [ci skip] add changefile * remove unwanted `unwrap` and `expect` * fmt
1 parent 852b40d commit 456a94f

File tree

5 files changed

+85
-26
lines changed

5 files changed

+85
-26
lines changed

.changes/fix-macos-updater.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Fix macOS `EXC_BAD_ACCESS` panic when app is code-signed.

.github/workflows/artifacts-updater.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,17 @@ jobs:
101101
yarn install
102102
node ../../tooling/cli.js/bin/tauri build
103103
env:
104-
TAURI_PRIVATE_KEY: dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5YTBGV3JiTy9lRDZVd3NkL0RoQ1htZmExNDd3RmJaNmRMT1ZGVjczWTBKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWdMekUzVkE4K0tWQ1hjeGt1Vkx2QnRUR3pzQjVuV0ZpM2czWXNkRm9hVUxrVnB6TUN3K1NheHJMREhQbUVWVFZRK3NIL1VsMDBHNW5ET1EzQno0UStSb21nRW4vZlpTaXIwZFh5ZmRlL1lSN0dKcHdyOUVPclVvdzFhVkxDVnZrbHM2T1o4Tk1NWEU9Cg==
104+
# Notarization (disabled)
105+
# FIXME: enable only on `dev` push maybe? as it take some times...
106+
#
107+
# APPLE_ID: ${{ secrets.APPLE_ID }}
108+
# APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
109+
110+
# Apple code signing testing
111+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
112+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
113+
# Updater signature
114+
TAURI_PRIVATE_KEY: ${{ secrets.UPDATER_PRIVATE_KEY }}
105115
- uses: actions/upload-artifact@v2
106116
if: matrix.platform == 'ubuntu-latest'
107117
with:
@@ -118,4 +128,4 @@ jobs:
118128
if: matrix.platform == 'macos-latest'
119129
with:
120130
name: macos-updater-artifacts
121-
path: ./target/release/bundle/macos/updater-example_*.app.tar.*
131+
path: ./target/release/bundle/macos/updater-example.app.tar.*

core/tauri/src/updater/core.rs

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ use std::{
1818
time::{SystemTime, UNIX_EPOCH},
1919
};
2020

21+
#[cfg(target_os = "macos")]
22+
use std::fs::rename;
2123
#[cfg(not(target_os = "macos"))]
2224
use std::process::Command;
2325

@@ -230,7 +232,7 @@ impl<'a> UpdateBuilder<'a> {
230232
} else {
231233
// we expect it to fail if we can't find the executable path
232234
// without this path we can't continue the update process.
233-
env::current_exe().expect("Can't access current executable path.")
235+
env::current_exe()?
234236
};
235237

236238
// Did the target is provided by the config?
@@ -479,17 +481,16 @@ impl Update {
479481
// We should have an AppImage already installed to be able to copy and install
480482
// the extract_path is the current AppImage path
481483
// tmp_dir is where our new AppImage is found
482-
483484
#[cfg(target_os = "linux")]
484485
fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Result {
485486
// we delete our current AppImage (we'll create a new one later)
486487
remove_file(&extract_path)?;
487488

488489
// In our tempdir we expect 1 directory (should be the <app>.app)
489-
let paths = read_dir(&tmp_dir).unwrap();
490+
let paths = read_dir(&tmp_dir)?;
490491

491492
for path in paths {
492-
let found_path = path.expect("Unable to extract").path();
493+
let found_path = path?.path();
493494
// make sure it's our .AppImage
494495
if found_path.extension() == Some(OsStr::new("AppImage")) {
495496
// Simply overwrite our AppImage (we use the command)
@@ -522,16 +523,15 @@ fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Resu
522523

523524
// ## EXE
524525
// Update server can provide a custom EXE (installer) who can run any task.
525-
526526
#[cfg(target_os = "windows")]
527527
#[allow(clippy::unnecessary_wraps)]
528528
fn copy_files_and_run(tmp_dir: tempfile::TempDir, _extract_path: PathBuf) -> Result {
529-
let paths = read_dir(&tmp_dir).unwrap();
529+
let paths = read_dir(&tmp_dir)?;
530530
// This consumes the TempDir without deleting directory on the filesystem,
531531
// meaning that the directory will no longer be automatically deleted.
532532
tmp_dir.into_path();
533533
for path in paths {
534-
let found_path = path.expect("Unable to extract").path();
534+
let found_path = path?.path();
535535
// we support 2 type of files exe & msi for now
536536
// If it's an `exe` we expect an installer not a runtime.
537537
if found_path.extension() == Some(OsStr::new("exe")) {
@@ -558,27 +558,57 @@ fn copy_files_and_run(tmp_dir: tempfile::TempDir, _extract_path: PathBuf) -> Res
558558
Ok(())
559559
}
560560

561-
// MacOS
561+
// Get the current app name in the path
562+
// Example; `/Applications/updater-example.app/Contents/MacOS/updater-example`
563+
// Should return; `updater-example.app`
564+
#[cfg(target_os = "macos")]
565+
fn macos_app_name_in_path(extract_path: &PathBuf) -> String {
566+
let components = extract_path.components();
567+
let app_name = components.last().unwrap();
568+
let app_name = app_name.as_os_str().to_str().unwrap();
569+
app_name.to_string()
570+
}
562571

572+
// MacOS
563573
// ### Expected structure:
564574
// ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler
565575
// │ └──[AppName].app # Main application
566576
// │ └── Contents # Application contents...
567577
// │ └── ...
568578
// └── ...
569-
570579
#[cfg(target_os = "macos")]
571580
fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Result {
572581
// In our tempdir we expect 1 directory (should be the <app>.app)
573-
let paths = read_dir(&tmp_dir).unwrap();
582+
let paths = read_dir(&tmp_dir)?;
583+
584+
// current app name in /Applications/<app>.app
585+
let app_name = macos_app_name_in_path(&extract_path);
574586

575587
for path in paths {
576-
let found_path = path.expect("Unable to extract").path();
588+
let mut found_path = path?.path();
577589
// make sure it's our .app
578590
if found_path.extension() == Some(OsStr::new("app")) {
579-
// Walk the temp dir and copy all files by replacing existing files only
580-
// and creating directories if needed
581-
Move::from_source(&found_path).walk_to_dest(&extract_path)?;
591+
let found_app_name = macos_app_name_in_path(&found_path);
592+
// make sure the app name in the archive matche the installed app name on path
593+
if found_app_name != app_name {
594+
// we need to replace the app name in the updater archive to match
595+
// installed app name
596+
let new_path = found_path.parent().unwrap().join(app_name);
597+
rename(&found_path, &new_path)?;
598+
599+
found_path = new_path;
600+
}
601+
602+
let sandbox_app_path = tempfile::Builder::new()
603+
.prefix("tauri_current_app_sandbox")
604+
.tempdir()?;
605+
606+
// Replace the whole application to make sure the
607+
// code signature is following
608+
Move::from_source(&found_path)
609+
.replace_using_temp(sandbox_app_path.path())
610+
.to_dest(&extract_path)?;
611+
582612
// early finish we have everything we need here
583613
return Ok(());
584614
}
@@ -619,7 +649,7 @@ pub fn extract_path_from_executable(executable_path: &Path) -> PathBuf {
619649
.expect("Can't determine extract path");
620650

621651
// MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp
622-
// We need to get /Applications/TestApp.app
652+
// We need to get /Applications/<app>.app
623653
// todo(lemarier): Need a better way here
624654
// Maybe we could search for <*.app> to get the right path
625655
#[cfg(target_os = "macos")]
@@ -691,22 +721,16 @@ pub fn verify_signature(
691721
let public_key = PublicKey::decode(pub_key_decoded)?;
692722
let signature_base64_decoded = base64_to_string(&release_signature)?;
693723

694-
let signature =
695-
Signature::decode(&signature_base64_decoded).expect("Something wrong with the signature");
724+
let signature = Signature::decode(&signature_base64_decoded)?;
696725

697726
// We need to open the file and extract the datas to make sure its not corrupted
698-
let file_open = OpenOptions::new()
699-
.read(true)
700-
.open(&archive_path)
701-
.expect("Can't open our archive to validate signature");
727+
let file_open = OpenOptions::new().read(true).open(&archive_path)?;
702728

703729
let mut file_buff: BufReader<File> = BufReader::new(file_open);
704730

705731
// read all bytes since EOF in the buffer
706732
let mut data = vec![];
707-
file_buff
708-
.read_to_end(&mut data)
709-
.expect("Can't read buffer to validate signature");
733+
file_buff.read_to_end(&mut data)?;
710734

711735
// Validate signature or bail out
712736
public_key.verify(&data, &signature)?;
@@ -779,6 +803,17 @@ mod test {
779803
}"#.into()
780804
}
781805

806+
#[cfg(target_os = "macos")]
807+
#[test]
808+
fn test_app_name_in_path() {
809+
let executable = extract_path_from_executable(Path::new(
810+
"/Applications/updater-example.app/Contents/MacOS/updater-example",
811+
));
812+
let app_name = macos_app_name_in_path(&executable);
813+
assert!(executable.ends_with("updater-example.app"));
814+
assert_eq!(app_name, "updater-example.app".to_string());
815+
}
816+
782817
#[test]
783818
fn simple_http_updater() {
784819
let _m = mockito::mock("GET", "/")
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.automation.apple-events</key><true/>
6+
</dict>
7+
</plist>

examples/updater/src-tauri/tauri.conf.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"useBootstrapper": false
2929
},
3030
"macOS": {
31+
"signingIdentity": "Developer ID Application: David Lemarier (3KF8V3679C)",
32+
"entitlements": "../entitlements.plist",
3133
"frameworks": [],
3234
"minimumSystemVersion": "",
3335
"useBootstrapper": false,

0 commit comments

Comments
 (0)