Repository: electron/electron
Pull request: test: MAS for private API usage

#### Description of Change

While working on the chromium roll, I had an idea to write a test to help catch potential MacOS private APIs that slip through to prevent regressions like https://github.com/electron/electron/issues/49616. The code was mostly generated, fwiw.

Here are the results when run against a Testing build of the non-MAS build:

```
Triggering runners: main

Running: Main process specs
Running: src/out/Testing/Electron.app/Contents/MacOS/Electron electron/spec
(node:12710) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
(Use `Electron --trace-deprecation ...` to show where the warning was created)
Trying to load the allocator multiple times. This is *not* supported.
ok 1 Mac App Store build private API usage should not use private macOS APIs in main process
not ok 2 Mac App Store build private API usage should not use private macOS APIs in Electron Framework
  Found private macOS APIs in Electron Framework:
  _abort_report_np
  _SetApplicationIsDaemon
  __LSSetApplicationLaunchServicesServerConnectionStatus
  _CGSSetWindowCaptureExcludeShape
  _CGRegionCreateWithRect
  _CTFontCopyVariationAxesInternal
  _AudioDeviceDuck
  _CGSMainConnectionID
  _IOBluetoothPreferenceSetControllerPowerState
  _CGSSetDenyWindowServerConnections
  _CGFontRenderingGetFontSmoothingDisabled
  _CTFontDescriptorIsSystemUIFont
  _sandbox_create_params
  _sandbox_set_param
  _sandbox_free_params
  _sandbox_compile_string
  _sandbox_free_profile
  _sandbox_extension_issue_file
  _sandbox_extension_consume
  _sandbox_extension_release
  __CSCheckFixDisable
  _responsibility_get_pid_responsible_for_pid
  
  These APIs are not allowed in Mac App Store builds and will cause rejection.
  See patches/chromium/mas_avoid_private_macos_api_usage.patch.patch
  Error: Found private macOS APIs in Electron Framework:
  _abort_report_np
  _SetApplicationIsDaemon
  __LSSetApplicationLaunchServicesServerConnectionStatus
  _CGSSetWindowCaptureExcludeShape
  _CGRegionCreateWithRect
  _CTFontCopyVariationAxesInternal
  _AudioDeviceDuck
  _CGSMainConnectionID
  _IOBluetoothPreferenceSetControllerPowerState
  _CGSSetDenyWindowServerConnections
  _CGFontRenderingGetFontSmoothingDisabled
  _CTFontDescriptorIsSystemUIFont
  _sandbox_create_params
  _sandbox_set_param
  _sandbox_free_params
  _sandbox_compile_string
  _sandbox_free_profile
  _sandbox_extension_issue_file
  _sandbox_extension_consume
  _sandbox_extension_release
  __CSCheckFixDisable
  _responsibility_get_pid_responsible_for_pid
  
  These APIs are not allowed in Mac App Store builds and will cause rejection.
  See patches/chromium/mas_avoid_private_macos_api_usage.patch.patch
      at Context.<anonymous> (electron/spec/mas-spec.ts:130:15)
      at processImmediate (node:internal/timers:504:21)
not ok 3 Mac App Store build private API usage should not use private macOS APIs in helper processes
  Found private macOS APIs in helper processes:
  
  Electron Helper:
    _abort_report_np
    _sandbox_init_with_parameters
    _sandbox_apply
  
  Electron Helper (Gpu):
    _abort_report_np
    _sandbox_init_with_parameters
    _sandbox_apply
  
  Electron Helper (Plugin):
    _abort_report_np
    _sandbox_init_with_parameters
    _sandbox_apply
  
  Electron Helper (Renderer):
    _abort_report_np
    _sandbox_init_with_parameters
    _sandbox_apply
  
  These APIs are not allowed in Mac App Store builds and will cause rejection.
  Error: Found private macOS APIs in helper processes:
  
  Electron Helper:
    _abort_report_np
    _sandbox_init_with_parameters
    _sandbox_apply
  
  Electron Helper (Gpu):
    _abort_report_np
    _sandbox_init_with_parameters
    _sandbox_apply
  
  Electron Helper (Plugin):
    _abort_report_np
    _sandbox_init_with_parameters
    _sandbox_apply
  
  Electron Helper (Renderer):
    _abort_report_np
    _sandbox_init_with_parameters
    _sandbox_apply
  
  These APIs are not allowed in Mac App Store builds and will cause rejection.
      at Context.<anonymous> (electron/spec/mas-spec.ts:158:15)
      at processImmediate (node:internal/timers:504:21)
not ok 4 Mac App Store build private API usage should not reference private Objective-C classes
  Found references to private Objective-C classes:
  
  Electron Framework:
    NSAccessibilityRemoteUIElement
    CAContext
  
  These are not allowed in Mac App Store builds and will cause rejection.
  Error: Found references to private Objective-C classes:
  
  Electron Framework:
    NSAccessibilityRemoteUIElement
    CAContext
  
  These are not allowed in Mac App Store builds and will cause rejection.
      at Context.<anonymous> (electron/spec/mas-spec.ts:214:15)
      at processImmediate (node:internal/timers:504:21)
# tests 4
# pass 1
# fail 3
1..4
[12710:0206/190155.474584:ERROR:base/process/kill_posix.cc:32] waitpid(12731): No child processes (10)
✗ Electron tests failed with code 3.
```

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included
- [ ] I have built and tested this PR
- [ ] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [ ] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)
- [ ] [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: none


Changed files:
filename: spec/mas-spec.ts
patch: @@ -0,0 +1,221 @@
+import * as cp from 'node:child_process';
+import * as path from 'node:path';
+
+import { ifdescribe } from './lib/spec-helpers';
+
+ifdescribe(process.platform === 'darwin' && process.mas)('Mac App Store build', () => {
+  describe('private API usage', () => {
+    // Get paths to all Electron binaries
+    const getElectronBinaries = () => {
+      const contentsPath = path.dirname(path.dirname(process.execPath));
+      return {
+        mainProcess: process.execPath,
+        framework: path.join(contentsPath, 'Frameworks', 'Electron Framework.framework', 'Electron Framework'),
+        helpers: {
+          main: path.join(contentsPath, 'Frameworks', 'Electron Helper.app', 'Contents', 'MacOS', 'Electron Helper'),
+          gpu: path.join(contentsPath, 'Frameworks', 'Electron Helper (GPU).app', 'Contents', 'MacOS', 'Electron Helper (GPU)'),
+          plugin: path.join(contentsPath, 'Frameworks', 'Electron Helper (Plugin).app', 'Contents', 'MacOS', 'Electron Helper (Plugin)'),
+          renderer: path.join(contentsPath, 'Frameworks', 'Electron Helper (Renderer).app', 'Contents', 'MacOS', 'Electron Helper (Renderer)')
+        }
+      };
+    };
+
+    const checkBinaryForPrivateAPIs = (binaryPath: string, binaryName: string) => {
+      // Use nm without -U to get all symbols (both defined and undefined)
+      const nmResult = cp.spawnSync('nm', ['-g', binaryPath], {
+        encoding: 'utf-8',
+        maxBuffer: 50 * 1024 * 1024
+      });
+
+      if (nmResult.error) {
+        throw new Error(`Failed to run nm on ${binaryName}: ${nmResult.error.message}`);
+      }
+
+      const symbols = nmResult.stdout;
+
+      // List of known private APIs that should not be present in MAS builds
+      // These are from the mas_avoid_private_macos_api_usage.patch
+      const privateAPIs = [
+        'abort_report_np',
+        'pthread_fchdir_np',
+        'pthread_chdir_np',
+        'SetApplicationIsDaemon',
+        '_LSSetApplicationLaunchServicesServerConnectionStatus',
+        'CGSSetWindowCaptureExcludeShape',
+        'CGRegionCreateWithRect',
+        'CTFontCopyVariationAxesInternal',
+        'AudioDeviceDuck',
+        'CGSMainConnectionID',
+        'IOBluetoothPreferenceSetControllerPowerState',
+        'CGSSetDenyWindowServerConnections',
+        'CGFontRenderingGetFontSmoothingDisabled',
+        'CTFontDescriptorIsSystemUIFont',
+        'sandbox_init_with_parameters',
+        'sandbox_create_params',
+        'sandbox_set_param',
+        'sandbox_free_params',
+        'sandbox_compile_string',
+        'sandbox_apply',
+        'sandbox_free_profile',
+        'sandbox_extension_issue_file',
+        'sandbox_extension_consume',
+        'sandbox_extension_release',
+        '_CSCheckFixDisable',
+        'responsibility_get_pid_responsible_for_pid',
+        '_NSAppendToKillRing',
+        '_NSPrependToKillRing',
+        '_NSYankFromKillRing',
+        '__NSNewKillRingSequence',
+        '__NSInitializeKillRing',
+        '_NSSetKillRingToYankedState'
+      ];
+
+      const foundPrivateAPIs: string[] = [];
+
+      for (const api of privateAPIs) {
+        // Check if the symbol appears in the nm output
+        // Look for ' U ' (undefined) or ' T ' (defined in text section) followed by the API
+        // nm output format is like: "                 U _symbol_name"
+        const regex = new RegExp(`\\s+[UT]\\s+(_${api})\\b`);
+        const match = symbols.match(regex);
+        if (match) {
+          // Keep the full symbol name including the underscore
+          foundPrivateAPIs.push(match[1]);
+        }
+      }
+
+      return foundPrivateAPIs;
+    };
+
+    it('should not use private macOS APIs in main process', function () {
+      this.timeout(60000);
+
+      const binaries = getElectronBinaries();
+      const foundPrivateAPIs = checkBinaryForPrivateAPIs(binaries.mainProcess, 'Electron main process');
+
+      if (foundPrivateAPIs.length > 0) {
+        throw new Error(
+          `Found private macOS APIs in main process:\n${foundPrivateAPIs.join('\n')}\n\n` +
+          'These APIs are not allowed in Mac App Store builds and will cause rejection.'
+        );
+      }
+
+      // Also check for private framework linkage
+      const otoolResult = cp.spawnSync('otool', ['-L', binaries.mainProcess], {
+        encoding: 'utf-8'
+      });
+
+      if (otoolResult.error) {
+        throw new Error(`Failed to run otool: ${otoolResult.error.message}`);
+      }
+
+      const frameworks = otoolResult.stdout;
+
+      if (frameworks.includes('PrivateFrameworks')) {
+        throw new Error(
+          'Found linkage to PrivateFrameworks which is not allowed in Mac App Store builds:\n' +
+          frameworks
+        );
+      }
+    });
+
+    it('should not use private macOS APIs in Electron Framework', function () {
+      this.timeout(60000);
+
+      // Check the Electron Framework binary (mentioned in issue #49616)
+      const binaries = getElectronBinaries();
+      const foundAPIs = checkBinaryForPrivateAPIs(binaries.framework, 'Electron Framework');
+
+      if (foundAPIs.length > 0) {
+        throw new Error(
+          `Found private macOS APIs in Electron Framework:\n${foundAPIs.join('\n')}\n\n` +
+          'These APIs are not allowed in Mac App Store builds and will cause rejection.\n' +
+          'See patches/chromium/mas_avoid_private_macos_api_usage.patch.patch'
+        );
+      }
+    });
+
+    it('should not use private macOS APIs in helper processes', function () {
+      this.timeout(60000);
+
+      const binaries = getElectronBinaries();
+      const allFoundAPIs: Record<string, string[]> = {};
+
+      for (const [helperName, helperPath] of Object.entries(binaries.helpers)) {
+        const displayName = `Electron Helper${helperName !== 'main' ? ` (${helperName.charAt(0).toUpperCase() + helperName.slice(1)})` : ''}`;
+        const foundAPIs = checkBinaryForPrivateAPIs(helperPath, displayName);
+
+        if (foundAPIs.length > 0) {
+          allFoundAPIs[displayName] = foundAPIs;
+        }
+      }
+
+      if (Object.keys(allFoundAPIs).length > 0) {
+        const errorLines = Object.entries(allFoundAPIs).map(([helper, apis]) =>
+          `${helper}:\n  ${apis.join('\n  ')}`
+        );
+
+        throw new Error(
+          `Found private macOS APIs in helper processes:\n\n${errorLines.join('\n\n')}\n\n` +
+          'These APIs are not allowed in Mac App Store builds and will cause rejection.'
+        );
+      }
+    });
+
+    it('should not reference private Objective-C classes', function () {
+      this.timeout(60000);
+
+      // Check for private Objective-C classes (appear as _OBJC_CLASS_$_ClassName)
+      const privateClasses = [
+        'NSAccessibilityRemoteUIElement',
+        'CAContext'
+      ];
+
+      const binaries = getElectronBinaries();
+      const binariesToCheck = [
+        { path: binaries.mainProcess, name: 'Electron main process' },
+        { path: binaries.framework, name: 'Electron Framework' },
+        { path: binaries.helpers.main, name: 'Electron Helper' },
+        { path: binaries.helpers.renderer, name: 'Electron Helper (Renderer)' }
+      ];
+
+      const foundClasses: Record<string, string[]> = {};
+
+      for (const binary of binariesToCheck) {
+        const nmResult = cp.spawnSync('nm', ['-g', binary.path], {
+          encoding: 'utf-8',
+          maxBuffer: 50 * 1024 * 1024
+        });
+
+        if (nmResult.error) {
+          throw new Error(`Failed to run nm on ${binary.name}: ${nmResult.error.message}`);
+        }
+
+        const symbols = nmResult.stdout;
+        const foundInBinary: string[] = [];
+
+        for (const className of privateClasses) {
+          // Look for _OBJC_CLASS_$_ClassName pattern
+          if (symbols.includes(`_OBJC_CLASS_$_${className}`)) {
+            foundInBinary.push(className);
+          }
+        }
+
+        if (foundInBinary.length > 0) {
+          foundClasses[binary.name] = foundInBinary;
+        }
+      }
+
+      if (Object.keys(foundClasses).length > 0) {
+        const errorLines = Object.entries(foundClasses).map(([binary, classes]) =>
+          `${binary}:\n  ${classes.join('\n  ')}`
+        );
+
+        throw new Error(
+          `Found references to private Objective-C classes:\n\n${errorLines.join('\n\n')}\n\n` +
+          'These are not allowed in Mac App Store builds and will cause rejection.'
+        );
+      }
+    });
+  });
+});