Repository: electron/electron
Pull request: fix: correct utility process exit code on Windows

On Windows, process exit codes are 32-bit unsigned integers (DWORD). When passed from Chromium to Electron as a signed int and then implicitly converted to uint64_t, values with the high bit set (e.g., NTSTATUS codes) undergo sign extension, producing incorrect values.

Cast the exit code to uint32_t before widening to uint64_t to prevent sign extension and preserve the original Windows exit code.

Fixes #49455

#### Description of Change
Earlier today I opened a PR for this issue, however, having used chat gpt to write its description, I'm afraid the electron bot tagged it as AI spam. That is not the case, the bug fix was a simple two line fix done by myself after investigating the cause. The problem was Chrome converts the windows error code from an unsigned 32 bit integer to a signed 32 bit integer, and then electron casts it to a 64 bit integer and does a sign extension. So for errors with highest bit set to 1 it will extend with 32 bits set to 1 on the left, giving us the unreal error codes. I ran the tests regarding the utilityProcess and all 55 tests passed.

Here you can see the print of the tests successfully passing: 
<img width="1449" height="679" alt="image" src="https://github.com/user-attachments/assets/b7b326a0-b57f-4660-8ac3-ec4c76c463d8" />

Thanks for your attention and sorry to have bothered you earlier today.

#### Checklist

- [x] PR description included
- [x] I have built and tested this PR
- [x] `npm test` passes


#### Release Notes

Notes: Fixed utilityProcess exit event reporting incorrect exit codes on Windows when the exit code has the high bit
  set.


Changed files:
filename: shell/browser/api/electron_api_utility_process.cc
patch: @@ -246,7 +246,7 @@ void UtilityProcessWrapper::OnServiceProcessLaunch(
   EmitWithoutEvent("spawn");
 }
 
-void UtilityProcessWrapper::HandleTermination(uint64_t exit_code) {
+void UtilityProcessWrapper::HandleTermination(uint32_t exit_code) {
   // HandleTermination is called from multiple callsites,
   // we need to ensure we only process it for the first callsite.
   if (terminated_)
@@ -314,7 +314,7 @@ void UtilityProcessWrapper::CloseConnectorPort() {
   }
 }
 
-void UtilityProcessWrapper::Shutdown(uint64_t exit_code) {
+void UtilityProcessWrapper::Shutdown(uint32_t exit_code) {
   node_service_remote_.reset();
   HandleTermination(exit_code);
 }

filename: shell/browser/api/electron_api_utility_process.h
patch: @@ -58,7 +58,7 @@ class UtilityProcessWrapper final
   static gin_helper::Handle<UtilityProcessWrapper> Create(gin::Arguments* args);
   static raw_ptr<UtilityProcessWrapper> FromProcessId(base::ProcessId pid);
 
-  void Shutdown(uint64_t exit_code);
+  void Shutdown(uint32_t exit_code);
 
   // gin_helper::Wrappable
   static gin::DeprecatedWrapperInfo kWrapperInfo;
@@ -78,7 +78,7 @@ class UtilityProcessWrapper final
   void OnServiceProcessLaunch(const base::Process& process);
   void CloseConnectorPort();
 
-  void HandleTermination(uint64_t exit_code);
+  void HandleTermination(uint32_t exit_code);
 
   void PostMessage(gin::Arguments* args);
   bool Kill();

filename: spec/api-utility-process-spec.ts
patch: @@ -129,6 +129,22 @@ describe('utilityProcess module', () => {
       expect(code).to.equal(exitCode);
     });
 
+    ifit(process.platform === 'win32')('emits correct exit code when high bit is set on Windows', async () => {
+      // NTSTATUS code with high bit set should not be mangled by sign extension.
+      const exitCode = 0xC0000005;
+      const child = utilityProcess.fork(path.join(fixturesPath, 'custom-exit.js'), [`--exitCode=${exitCode}`]);
+      const [code] = await once(child, 'exit');
+      expect(code).to.equal(exitCode);
+    });
+
+    ifit(process.platform !== 'win32')('emits correct exit code when child process crashes on posix', async () => {
+      // Crash exit codes should not be sign-extended to large 64-bit values.
+      const child = utilityProcess.fork(path.join(fixturesPath, 'crash.js'));
+      const [code] = await once(child, 'exit');
+      expect(code).to.not.equal(0);
+      expect(code).to.be.lessThanOrEqual(0xFFFFFFFF);
+    });
+
     it('does not run JS after process.exit is called', async () => {
       const file = path.join(os.tmpdir(), `no-js-after-exit-log-${Math.random()}`);
       const child = utilityProcess.fork(path.join(fixturesPath, 'no-js-after-exit.js'), [`--testPath=${file}`]);