Browse Source

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
david 4 năm trước cách đây
mục cha
commit
456a94f663

+ 5 - 0
.changes/fix-macos-updater.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Fix macOS `EXC_BAD_ACCESS` panic when app is code-signed.

+ 12 - 2
.github/workflows/artifacts-updater.yml

@@ -101,7 +101,17 @@ jobs:
           yarn install
           node ../../tooling/cli.js/bin/tauri build
         env:
-          TAURI_PRIVATE_KEY: dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5YTBGV3JiTy9lRDZVd3NkL0RoQ1htZmExNDd3RmJaNmRMT1ZGVjczWTBKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWdMekUzVkE4K0tWQ1hjeGt1Vkx2QnRUR3pzQjVuV0ZpM2czWXNkRm9hVUxrVnB6TUN3K1NheHJMREhQbUVWVFZRK3NIL1VsMDBHNW5ET1EzQno0UStSb21nRW4vZlpTaXIwZFh5ZmRlL1lSN0dKcHdyOUVPclVvdzFhVkxDVnZrbHM2T1o4Tk1NWEU9Cg==
+          # Notarization (disabled)
+          # FIXME: enable only on `dev` push maybe? as it take some times...
+          #
+          # APPLE_ID: ${{ secrets.APPLE_ID }}
+          # APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
+
+          # Apple code signing testing
+          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
+          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+          # Updater signature
+          TAURI_PRIVATE_KEY: ${{ secrets.UPDATER_PRIVATE_KEY }}
       - uses: actions/upload-artifact@v2
         if: matrix.platform == 'ubuntu-latest'
         with:
@@ -118,4 +128,4 @@ jobs:
         if: matrix.platform == 'macos-latest'
         with:
           name: macos-updater-artifacts
-          path: ./target/release/bundle/macos/updater-example_*.app.tar.*
+          path: ./target/release/bundle/macos/updater-example.app.tar.*

+ 59 - 24
core/tauri/src/updater/core.rs

@@ -18,6 +18,8 @@ use std::{
   time::{SystemTime, UNIX_EPOCH},
 };
 
+#[cfg(target_os = "macos")]
+use std::fs::rename;
 #[cfg(not(target_os = "macos"))]
 use std::process::Command;
 
@@ -230,7 +232,7 @@ impl<'a> UpdateBuilder<'a> {
     } else {
       // we expect it to fail if we can't find the executable path
       // without this path we can't continue the update process.
-      env::current_exe().expect("Can't access current executable path.")
+      env::current_exe()?
     };
 
     // Did the target is provided by the config?
@@ -479,17 +481,16 @@ impl Update {
 // We should have an AppImage already installed to be able to copy and install
 // the extract_path is the current AppImage path
 // tmp_dir is where our new AppImage is found
-
 #[cfg(target_os = "linux")]
 fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Result {
   // we delete our current AppImage (we'll create a new one later)
   remove_file(&extract_path)?;
 
   // In our tempdir we expect 1 directory (should be the <app>.app)
-  let paths = read_dir(&tmp_dir).unwrap();
+  let paths = read_dir(&tmp_dir)?;
 
   for path in paths {
-    let found_path = path.expect("Unable to extract").path();
+    let found_path = path?.path();
     // make sure it's our .AppImage
     if found_path.extension() == Some(OsStr::new("AppImage")) {
       // 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
 
 // ## EXE
 // Update server can provide a custom EXE (installer) who can run any task.
-
 #[cfg(target_os = "windows")]
 #[allow(clippy::unnecessary_wraps)]
 fn copy_files_and_run(tmp_dir: tempfile::TempDir, _extract_path: PathBuf) -> Result {
-  let paths = read_dir(&tmp_dir).unwrap();
+  let paths = read_dir(&tmp_dir)?;
   // This consumes the TempDir without deleting directory on the filesystem,
   // meaning that the directory will no longer be automatically deleted.
   tmp_dir.into_path();
   for path in paths {
-    let found_path = path.expect("Unable to extract").path();
+    let found_path = path?.path();
     // we support 2 type of files exe & msi for now
     // If it's an `exe` we expect an installer not a runtime.
     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
   Ok(())
 }
 
-// MacOS
+// Get the current app name in the path
+// Example; `/Applications/updater-example.app/Contents/MacOS/updater-example`
+// Should return; `updater-example.app`
+#[cfg(target_os = "macos")]
+fn macos_app_name_in_path(extract_path: &PathBuf) -> String {
+  let components = extract_path.components();
+  let app_name = components.last().unwrap();
+  let app_name = app_name.as_os_str().to_str().unwrap();
+  app_name.to_string()
+}
 
+// MacOS
 // ### Expected structure:
 // ├── [AppName]_[version]_x64.app.tar.gz       # GZ generated by tauri-bundler
 // │   └──[AppName].app                         # Main application
 // │      └── Contents                          # Application contents...
 // │          └── ...
 // └── ...
-
 #[cfg(target_os = "macos")]
 fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Result {
   // In our tempdir we expect 1 directory (should be the <app>.app)
-  let paths = read_dir(&tmp_dir).unwrap();
+  let paths = read_dir(&tmp_dir)?;
+
+  // current app name in /Applications/<app>.app
+  let app_name = macos_app_name_in_path(&extract_path);
 
   for path in paths {
-    let found_path = path.expect("Unable to extract").path();
+    let mut found_path = path?.path();
     // make sure it's our .app
     if found_path.extension() == Some(OsStr::new("app")) {
-      // Walk the temp dir and copy all files by replacing existing files only
-      // and creating directories if needed
-      Move::from_source(&found_path).walk_to_dest(&extract_path)?;
+      let found_app_name = macos_app_name_in_path(&found_path);
+      // make sure the app name in the archive matche the installed app name on path
+      if found_app_name != app_name {
+        // we need to replace the app name in the updater archive to match
+        // installed app name
+        let new_path = found_path.parent().unwrap().join(app_name);
+        rename(&found_path, &new_path)?;
+
+        found_path = new_path;
+      }
+
+      let sandbox_app_path = tempfile::Builder::new()
+        .prefix("tauri_current_app_sandbox")
+        .tempdir()?;
+
+      // Replace the whole application to make sure the
+      // code signature is following
+      Move::from_source(&found_path)
+        .replace_using_temp(sandbox_app_path.path())
+        .to_dest(&extract_path)?;
+
       // early finish we have everything we need here
       return Ok(());
     }
@@ -619,7 +649,7 @@ pub fn extract_path_from_executable(executable_path: &Path) -> PathBuf {
     .expect("Can't determine extract path");
 
   // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp
-  // We need to get /Applications/TestApp.app
+  // We need to get /Applications/<app>.app
   // todo(lemarier): Need a better way here
   // Maybe we could search for <*.app> to get the right path
   #[cfg(target_os = "macos")]
@@ -691,22 +721,16 @@ pub fn verify_signature(
   let public_key = PublicKey::decode(pub_key_decoded)?;
   let signature_base64_decoded = base64_to_string(&release_signature)?;
 
-  let signature =
-    Signature::decode(&signature_base64_decoded).expect("Something wrong with the signature");
+  let signature = Signature::decode(&signature_base64_decoded)?;
 
   // We need to open the file and extract the datas to make sure its not corrupted
-  let file_open = OpenOptions::new()
-    .read(true)
-    .open(&archive_path)
-    .expect("Can't open our archive to validate signature");
+  let file_open = OpenOptions::new().read(true).open(&archive_path)?;
 
   let mut file_buff: BufReader<File> = BufReader::new(file_open);
 
   // read all bytes since EOF in the buffer
   let mut data = vec![];
-  file_buff
-    .read_to_end(&mut data)
-    .expect("Can't read buffer to validate signature");
+  file_buff.read_to_end(&mut data)?;
 
   // Validate signature or bail out
   public_key.verify(&data, &signature)?;
@@ -779,6 +803,17 @@ mod test {
     }"#.into()
   }
 
+  #[cfg(target_os = "macos")]
+  #[test]
+  fn test_app_name_in_path() {
+    let executable = extract_path_from_executable(Path::new(
+      "/Applications/updater-example.app/Contents/MacOS/updater-example",
+    ));
+    let app_name = macos_app_name_in_path(&executable);
+    assert!(executable.ends_with("updater-example.app"));
+    assert_eq!(app_name, "updater-example.app".to_string());
+  }
+
   #[test]
   fn simple_http_updater() {
     let _m = mockito::mock("GET", "/")

+ 7 - 0
examples/updater/entitlements.plist

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>com.apple.security.automation.apple-events</key><true/>
+  </dict>
+</plist>

+ 2 - 0
examples/updater/src-tauri/tauri.conf.json

@@ -28,6 +28,8 @@
         "useBootstrapper": false
       },
       "macOS": {
+        "signingIdentity": "Developer ID Application: David Lemarier (3KF8V3679C)",
+        "entitlements": "../entitlements.plist",
         "frameworks": [],
         "minimumSystemVersion": "",
         "useBootstrapper": false,