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

Backport of #47271

See that PR for details.


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")
+)