Repository: electron/electron
Pull request: fix: Squirrel.Mac crash when zip extraction fails

#### Description of Change

Fixes https://github.com/electron/electron/issues/47270

Squirrel.Mac launches `/usr/bin/ditto` to extract the update zip file. When that process fails to launch, the (deprecated) [`launch` function](https://developer.apple.com/documentation/foundation/process/launch()?language=objc) will raise an `NSInvalidArgumentException`. This exception wasn't caught. Thus, the app crashed.

This PR migrates the code to the (non-deprecated) [`launchAndReturnError` function](https://developer.apple.com/documentation/foundation/process/run()?language=objc) and adds proper error handling.

I've also included a test to prevent this from regressing.

This is the first Objective-C code I've ever written, so please be extra careful during your review.

I've tested the change as follows:

<details><summary>All autoUpdater tests pass (incl. the newly added test).</summary>
<p>

```
nikwen@MacBook spec % e test --no-remote -g "autoUpdater"
Running "/usr/local/bin/node ./script/spec-runner.js -g autoUpdater" in /Volumes/Electron/electron/src/electron
Triggering runners: main

Running: Main process specs
ok 1 autoUpdater module checkForUpdates emits an error on Windows if the feed URL is not set # SKIP -
ok 2 autoUpdater module getFeedURL returns an empty string by default
ok 3 autoUpdater module getFeedURL correctly fetches the previously set FeedURL # SKIP -
ok 4 autoUpdater module setFeedURL on Mac or Windows sets url successfully using old (url, headers) syntax
ok 5 autoUpdater module setFeedURL on Mac or Windows throws if no url is provided when using the old style
ok 6 autoUpdater module setFeedURL on Mac or Windows sets url successfully using new ({ url }) syntax
ok 7 autoUpdater module setFeedURL on Mac or Windows throws if no url is provided when using the new style
ok 8 autoUpdater module setFeedURL on Mac emits an error when the application is unsigned # SKIP -
ok 9 autoUpdater module setFeedURL on Mac does not throw if default is the serverType # SKIP -
ok 10 autoUpdater module setFeedURL on Mac does not throw if json is the serverType # SKIP -
ok 11 autoUpdater module setFeedURL on Mac does throw if an unknown string is the serverType # SKIP -
ok 12 autoUpdater module quitAndInstall emits an error on Windows when no update is available # SKIP -
ok 13 autoUpdater behavior should have a valid code signing identity
ok 14 autoUpdater behavior should fail to set the feed URL when the app is not signed # SKIP -
ok 15 autoUpdater behavior should cleanly set the feed URL when the app is signed
ok 16 autoUpdater behavior with update server should hit the update endpoint when checkForUpdates is called
ok 17 autoUpdater behavior with update server should hit the update endpoint with customer headers when checkForUpdates is called
ok 18 autoUpdater behavior with update server should hit the download endpoint when an update is available and error if the file is bad
ok 19 autoUpdater behavior with update server should hit the download endpoint when an update is available and update successfully when the zip is provided
ok 20 autoUpdater behavior with update server should hit the download endpoint when an update is available and update successfully when the zip is provided even after a different update was staged
ok 21 autoUpdater behavior with update server should update to lower version numbers
ok 22 autoUpdater behavior with update server should abort the update if the application is still running when ShipIt kicks off
ok 23 autoUpdater behavior with update server should hit the download endpoint when an update is available and fail when the zip signature is invalid
ok 24 autoUpdater behavior with update server should hit the download endpoint when an update is available and fail when the ShipIt binary is a symlink
ok 25 autoUpdater behavior with update server should hit the download endpoint when an update is available and fail when the Electron Framework is modified
ok 26 autoUpdater behavior with update server should hit the download endpoint when an update is available and fail when the zip extraction process fails to launch
ok 27 autoUpdater behavior with update server should hit the download endpoint when an update is available and update successfully when the zip is provided with JSON update mode
ok 28 autoUpdater behavior with update server should hit the download endpoint when an update is available and not update in JSON update mode when the currentRelease is older than the current version
ok 29 autoUpdater behavior with update server with ElectronSquirrelPreventDowngrades enabled should not update to lower version numbers
ok 30 autoUpdater behavior with update server with ElectronSquirrelPreventDowngrades enabled should not update to version strings that are not simple Major.Minor.Patch
ok 31 autoUpdater behavior with update server with ElectronSquirrelPreventDowngrades enabled should still update to higher version numbers
ok 32 autoUpdater behavior with update server with ElectronSquirrelPreventDowngrades enabled should compare version numbers correctly
ok 33 autoUpdater behavior with update server with SquirrelMacEnableDirectContentsWrite enabled should hit the download endpoint when an update is available and update successfully when the zip is provided leaving the parent directory untouched
# tests 25
# pass 25
# fail 0
1..33
[1755:0527/181855.901631:ERROR:base/process/kill_posix.cc:33] waitpid(1757): No child processes (10)
✓ Electron main process tests passed.
```

</p>
</details> 

<details><summary>The newly added test crashes with SIGTRAP on main.</summary>
<p>

```
nikwen@MacBook spec % e test --no-remote -g "zip extraction process" --electronVersion 38.0.0-nightly.20250526 
Running "/usr/local/bin/node ./script/spec-runner.js -g zip extraction process --electronVersion=38.0.0-nightly.20250526" in /Volumes/Electron/electron/src/electron
Only running: [ 'main' ]
Running against Electron undefined

Running: Main process specs
Unhandled exception in main spec runner: AssertionError: expected 'SIGTRAP' to equal null
    at ChildProcess.<anonymous> (/Volumes/Electron/electron/src/electron/spec/lib/codesign-helpers.ts:72:25)
    at ChildProcess.emit (node:events:518:28)
    at Process.ChildProcess._handle.onexit (node:internal/child_process:293:12) {
  showDiff: true,
  actual: 'SIGTRAP',
  expected: null,
  operator: 'strictEqual'
}
not ok 1 autoUpdater behavior with update server should hit the download endpoint when an update is available and fail when the zip extraction process fails to launch
  expected 'SIGTRAP' to equal null
  AssertionError: expected 'SIGTRAP' to equal null
      at ChildProcess.<anonymous> (electron/spec/lib/codesign-helpers.ts:72:25)
      at ChildProcess.emit (node:events:518:28)
      at Process.ChildProcess._handle.onexit (node:internal/child_process:293:12)
✗ Electron tests failed with code 1.
ERROR Error: Command failed: /usr/local/bin/node ./script/spec-runner.js -g zip extraction process --electronVersion=38.0.0-nightly.20250526
    at genericNodeError (node:internal/errors:984:15)
    at wrappedFn (node:internal/errors:538:14)
    at checkExecSyncError (node:child_process:891:11)
    at Object.execFileSync (node:child_process:927:15)
    at runSpecRunner (/Users/nikwen/.electron_build_tools/src/e-test.js:33:16)
    at Command.<anonymous> (/Users/nikwen/.electron_build_tools/src/e-test.js:76:7)
    at Command.listener [as _actionHandler] (/Users/nikwen/.electron_build_tools/node_modules/commander/lib/command.js:480:17)
    at /Users/nikwen/.electron_build_tools/node_modules/commander/lib/command.js:1234:65
    at Command._chainOrCall (/Users/nikwen/.electron_build_tools/node_modules/commander/lib/command.js:1151:12)
    at Command._parseCommand (/Users/nikwen/.electron_build_tools/node_modules/commander/lib/command.js:1234:27)
```

</p>
</details> 

CC @MarshallOfSound

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed crash in `autoUpdater` on macOS when zip extraction failed.


Changed files:
filename: patches/squirrel.mac/.patches
patch: @@ -7,3 +7,4 @@ fix_abort_installation_attempt_at_the_final_mile_if_the_app_is.patch
 feat_add_ability_to_prevent_version_downgrades.patch
 refactor_use_non-deprecated_nskeyedarchiver_apis.patch
 chore_turn_off_launchapplicationaturl_deprecation_errors_in_squirrel.patch
+fix_crash_when_process_to_extract_zip_cannot_be_launched.patch

filename: patches/squirrel.mac/fix_crash_when_process_to_extract_zip_cannot_be_launched.patch
patch: @@ -0,0 +1,30 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Niklas Wenzel <dev@nikwen.de>
+Date: Tue, 27 May 2025 02:03:54 +0200
+Subject: fix: crash when process to extract zip cannot be launched
+
+Fixes https://github.com/electron/electron/issues/47270
+
+diff --git a/Squirrel/SQRLZipArchiver.m b/Squirrel/SQRLZipArchiver.m
+index 68f5dac8e553638f41306956df9d38eeda18f8f2..a9cd676df63e19edf9e20473d27b85591c7cb49e 100644
+--- a/Squirrel/SQRLZipArchiver.m
++++ b/Squirrel/SQRLZipArchiver.m
+@@ -153,7 +153,17 @@ - (RACSignal *)launchWithArguments:(NSArray *)arguments {
+ 		setNameWithFormat:@"-launchWithArguments: %@", arguments];
+ 
+ 	self.dittoTask.arguments = arguments;
+-	[self.dittoTask launch];
++
++	NSError *launchError = nil;
++
++	if (![self.dittoTask launchAndReturnError:&launchError]) {
++		NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
++		userInfo[NSLocalizedDescriptionKey] = launchError.localizedDescription;
++
++		NSLog(@"Starting ditto task failed with error: %@", launchError.localizedDescription);
++
++		return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
++	}
+ 
+ 	return signal;
+ }

filename: spec/api-autoupdater-darwin-spec.ts
patch: @@ -42,6 +42,16 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
     return cp.spawn(path.resolve(appPath, 'Contents/MacOS/Electron'), args);
   };
 
+  const launchAppSandboxed = (appPath: string, profilePath: string, args: string[] = []) => {
+    return spawn('/usr/bin/sandbox-exec', [
+      '-f',
+      profilePath,
+      path.resolve(appPath, 'Contents/MacOS/Electron'),
+      ...args,
+      '--no-sandbox'
+    ]);
+  };
+
   const getRunningShipIts = async (appPath: string) => {
     const processes = await psList();
     const activeShipIts = processes.filter(p => p.cmd?.includes('Squirrel.framework/Resources/ShipIt com.github.Electron.ShipIt') && p.cmd!.startsWith(appPath));
@@ -740,6 +750,41 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
       });
     });
 
+    it('should hit the download endpoint when an update is available and fail when the zip extraction process fails to launch', async () => {
+      await withUpdatableApp({
+        nextVersion: '2.0.0',
+        startFixture: 'update',
+        endFixture: 'update'
+      }, async (appPath, updateZipPath) => {
+        server.get('/update-file', (req, res) => {
+          res.download(updateZipPath);
+        });
+        server.get('/update-check', (req, res) => {
+          res.json({
+            url: `http://localhost:${port}/update-file`,
+            name: 'My Release Name',
+            notes: 'Theses are some release notes innit',
+            pub_date: (new Date()).toString()
+          });
+        });
+        const launchResult = await launchAppSandboxed(
+          appPath,
+          path.resolve(__dirname, 'fixtures/auto-update/sandbox/block-ditto.sb'),
+          [`http://localhost:${port}/update-check`]
+        );
+        logOnError(launchResult, () => {
+          expect(launchResult).to.have.property('code', 1);
+          expect(launchResult.out).to.include('Starting ditto task failed with error:');
+          expect(launchResult.out).to.include('SQRLZipArchiverErrorDomain');
+          expect(requests).to.have.lengthOf(2);
+          expect(requests[0]).to.have.property('url', '/update-check');
+          expect(requests[1]).to.have.property('url', '/update-file');
+          expect(requests[0].header('user-agent')).to.include('Electron/');
+          expect(requests[1].header('user-agent')).to.include('Electron/');
+        });
+      });
+    });
+
     it('should hit the download endpoint when an update is available and update successfully when the zip is provided with JSON update mode', async () => {
       await withUpdatableApp({
         nextVersion: '2.0.0',

filename: spec/fixtures/auto-update/sandbox/block-ditto.sb
patch: @@ -0,0 +1,5 @@
+(version 1)
+(allow default)
+(deny process-exec
+  (literal "/usr/bin/ditto")
+)