Skip to content

fix(desktop): give macOS executables their own LC_UUID - #6027

Open
sosyz wants to merge 3 commits into
apache:mainfrom
sosyz:fix/macos-executable-uuid
Open

sosyz wants to merge 3 commits into
apache:mainfrom
sosyz:fix/macos-executable-uuid

Conversation

@sosyz

@sosyz sosyz commented Oct 9, 2026

Copy link
Copy Markdown
Member

Summary

electron-builder renames Electron's prebuilt executables without relinking them, so Maka and its four helpers shipped Electron 43.4.1's stock Mach-O LC_UUID. Every other app on that Electron release has the same UUID. macOS Local Network Privacy identifies a program by that UUID, so a grant or denial for another app could apply to Maka. Details and a real collision are in the issue.

  • New afterPack hook (scripts/macos-executable-uuid.mjs). It rewrites the LC_UUID of the main executable and every helper app before signing, so the stamped binaries are what electron-builder signs and notarizes.
  • The new UUID is a UUIDv5 of appId/executable name/CPU type. It is deterministic rather than random, so the UUID stays the same across releases and a user's Local Network decision survives updates. arm64 and x64 builds get different UUIDs.
  • verify:macos now rejects a package whose executables lack the expected UUID, so a hook that stops running fails the release check instead of shipping the stock UUID.
  • Windows and Linux packaging are unaffected because the hook returns early off darwin.

Fixes #6026

Verification

  • node --test scripts/macos-executable-uuid.test.mjs: 12 passing. Covers synthetic thin and fat Mach-O, idempotence, rejection of the stock UUID, and Electron's real arm64 executable on macOS.
  • node --test scripts/verify-packaged-app.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-release-targets.test.mjs scripts/ci-test-plan.test.mjs scripts/macos-executable-uuid.test.mjs: 99/99 passing.
  • biome lint ., biome format . and knip --workspace apps/desktop are clean.
  • Ran a local electron-builder --mac dir --arm64 (ad-hoc signed, not notarized):
    • dwarfdump --uuid shows all five executables with new UUIDs. The main executable went from 4C4C4470-5555-3144-A124-518576DD11E3 to E08E3A84-68A1-5189-9663-A4FEC17A5EE5.
    • codesign --verify --deep --strict passes.
    • The new release check accepts the package.
    • The app launches and spawns its helper processes.
  • Not run: a full signed and notarized package:macos-* / verify:macos, which needs the release signing credentials.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Code (Claude Opus 5.5) did the investigation, implementation and tests. A human reviewed the change. The commit carries a Generated-by trailer.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

🤖 Generated with Claude Code

@github-actions github-actions Bot added the effort/M Under 500 readable lines label Oct 9, 2026

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Reviewed range: merge-base 6fa4851 (main) to head 59e615a, 2 commits, 9 files, +408/-7. This is a first review, so there are no prior findings.

Verdict: I found no P0-P2 defects. Three P3 notes are below.

What I verified

  • Hook order. In the pinned electron-builder 26.17.0, platformPackager.doPack runs the config afterPack, then the sanity check, then doAddElectronFuses, then doSignAfterPack. The stamped executables are what get signed (hardened runtime, Developer ID) and notarized, so the byte edit doesn't break the signature.
  • No unsigned arm64 output. Electron's stock arm64 executable is only linker-signed, and rewriting LC_UUID invalidates that signature. If electron-builder ever skipped signing afterwards, the result would be an arm64 app the kernel kills at launch. The mac config sets forceCodeSigning: true, so a missing identity (reportError) or identity: null (handleNullIdentity) fails the build instead. The ad-hoc identity: '-' path still signs. I don't see a supported path that ships a stamped, unsigned binary.
  • Fuses don't overlap. Fuses are flipped in Electron Framework, which this hook doesn't touch.
  • Parser. It handles thin MH_MAGIC_64 and FAT_MAGIC 32-bit fat headers. It replaces the 16 UUID bytes in place, keeps the file length, and is idempotent. Out-of-range reads throw. FAT_MAGIC_64 isn't handled, but nothing ships it, so there is no impact.
  • UUID derivation. The UUID is UUIDv5-style SHA-1 over a fixed namespace plus appId/executable/cputype. appId comes from packager.appInfo.id (com.maka.desktop) when stamping. The verifier reads it from CFBundleIdentifier after asserting it matches the expected id, so both sides agree. Nightly and formal share the app id, so they share UUIDs. That matches the documented "same application identity" policy.
  • Verifier coverage. verify:macos calls assertMacAppExecutableUuids before codesign --verify, so a hook that stops running fails the release check.
  • No crash-symbolication cost. No crashReporter/Sentry/dSYM pipeline exists in the repo, so moving away from Electron's published UUIDs costs nothing there.
  • Supply chain and signing. There are no new dependencies and no lockfile change. No .github/workflows files changed. No signing secrets, entitlements or notarization settings were touched. The only supply-chain-relevant effect is intended: the shipped main and helper executables are no longer byte-identical to upstream Electron's prebuilt binaries. They differ only in the 16 LC_UUID bytes per slice, before signing. I found no release check that compares Desktop Mach-O bytes against upstream Electron. The RELEASE.json Mach-O inventory covers the CLI ZIP only.

Findings

  1. P3 (CI coverage). PR CI does not exercise the macOS change.
    • The CI test job runs check:release on ubuntu-24.04. There, the real-Electron test (rewrites the UUID of Electron's own executable) is skipped, and only the synthetic-buffer tests run.
    • The package check on this PR is the Windows release check. There the hook only proves it returns early off darwin.
    • The owner (macos-latest) job is the Runtime Host owner-platform check and does not package the Desktop app.
    • The first real run of the hook is post-merge, in the signed Desktop Nightly on macos-15 (arm64) and macos-15-intel (x64), followed by verify:macos. A failure there blocks the nightly rather than shipping a bad build.
    • The x64 path, and the full signed and notarized flow on either arch, have not been run by anyone. The PR states this. Please watch the first nightly after merge.
  2. P3 (behavior change worth a release note). Every existing install currently carries Electron 43.4.1's stock UUID. The first update with this change moves Maka to a new, stable UUID. Users will likely see the Local Network prompt once more, and any earlier grant or denial recorded under the stock UUID stays attached to that shared UUID. That is the intended fix, but users should be told about the one-time re-prompt.
  3. P3 (permanence). MAKA_EXECUTABLE_UUID_NAMESPACE and the appId/executableName/cputype name format are now a persistent identity contract, like appId. Renaming productName (which renames the main and helper executables) or changing the format re-keys every user's Local Network decision. The comment covers the namespace, but nothing pins the derived values. A golden-value test, for example the expected UUID for com.maka.desktop/Maka/arm64, would catch an accidental change.

Protocol/epoch: No protocol files are touched, so there is no epoch impact.

Mergeability/CI: GitHub reports MERGEABLE, with merge state BLOCKED (awaiting required review). All six checks on 59e615a pass: audit, owner (macos-latest), owner (windows-latest), package (Release Windows check), package-linux, and test. The Windows test inventory was updated for the new darwin-only test, and Check Windows test inventory passes.

artifactName: 'Maka-${version}-mac-${arch}.${ext}',
asar: true,
beforePack: stageReleaseManifests,
afterPack: stampMacExecutableUuids,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 note: I verified that in electron-builder 26.17.0 the config afterPack runs before fuses and doSignAfterPack, and mac.forceCodeSigning: true prevents an unsigned (linker-signature-invalidated) arm64 build. PR CI never runs this hook on macOS, though. The first real run is the post-merge signed Desktop Nightly (macos-15 / macos-15-intel), so please watch that run.


// Fixed namespace for the UUIDv5 names below. Changing it changes every
// shipped UUID, which macOS treats as a different program.
const MAKA_EXECUTABLE_UUID_NAMESPACE = Buffer.from('4195730630d44e84a842a14db6f67a8f', 'hex');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: this namespace and the appId/executableName/cputype name format are now a persistent identity contract, as is productName, because it names the executables. Consider a golden-value test, for example the expected UUID for com.maka.desktop/Maka/arm64, so a refactor can't silently re-key users' Local Network decisions.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Reviewed head 59e615a96c758bf7a8e0a1e4379c4f37dadf3267 against merge-base 6fa48516106b4e2de591aea4a96a538425c8aafa: 2 commits, 9 files, +408/-7.

I found no verified P0–P2 defect. This does not establish that the macOS permission behavior has been validated.

The installed app-builder-lib 26.17.0 invokes afterPack before fuses and signing. The normal macOS release wrapper requires signing/notarization credentials, and forceCodeSigning rejects a missing identity or explicit identity: null. I did not find a supported production path that ships the edited executable without re-signing. The actual configuration hook stamps the main and helper executables in a synthetic Darwin bundle and is a no-op off Darwin. The verifier uses the bundle identifier after checking its expected value. Nightly and formal builds share that identifier.

Earlier review notes: the missing signed/notarized macOS release evidence remains a P3 follow-up (apps/desktop/electron-builder.config.mjs:87, scripts/macos-executable-uuid.test.mjs:156). The author reports an ad-hoc-signed arm64 launch, not a full release. A golden-value UUID test is still useful. However, changing a UUID has not by itself been shown to reset every user's permission or produce a one-time re-prompt: Apple's current guidance also identifies applications through code signatures. Please present those consequences as unverified until tested. Apple TN3179

Two additional P3 follow-ups:

  • scripts/macos-executable-uuid.mjs:48: the derivation discards native-build identity. Different native payloads and upstream UUIDs receive the same stamped UUID for a fixed app/name/CPU. Apple's build-UUID guidance distinguishes different native images for symbol matching. Please retain build-specific identity in the derivation, or explain and validate the deliberate tradeoff. The absence of a repository crash-upload pipeline does not establish that OS crash reports or external symbol matching have no cost. I have not reproduced an application crash or a Local Network regression, so this is not a P2 claim. Apple TN3178
  • scripts/macos-executable-uuid.mjs:65: a FAT header with zero slices produces an empty UUID list, and the UUID assertion accepts a helper containing only that header. An LC_UUID with cmdsize=8 is also accepted and stamped. Reject empty/out-of-bounds slice tables and require a complete 24-byte UUID command inside its declared slice/load-command region. These are malformed-input hardening cases, not a demonstrated bypass of subsequent signature, Gatekeeper or launch checks.

Local verification: Core, Storage, MCP and Runtime builds passed; the five changed/related script suites passed 98 tests with 1 macOS-only skip; Windows inventory passed 3 tests and is current at 121 declarations; scoped Biome and diff whitespace checks passed. An additional probe exercised the actual configuration hook, the native-image UUID collision and malformed helper acceptance. I ran these on Linux, not macOS. I did not execute Developer ID signing, notarization, Gatekeeper assessment, a DMG launch, or before/after-update Local Network grant/deny tests. Both supported architectures still need real release evidence.

No protocol files change; there is no compatibility-epoch impact.

All six checks on this exact head are successful, including CI. The macOS check tests the Runtime Host local-deployment owner, not Desktop packaging. The PR is now CONFLICTING/DIRTY against main c11a5336b; a read-only merge confirms the sole conflict is docs/windows-test-inventory.md. Rebase, regenerate that inventory and rerun checks before merging. Earlier MERGEABLE evidence is no longer current.

* the CPU type keeps the arm64 and x64 builds apart.
*/
export function expectedExecutableUuid({ appId, executableName, cputype }) {
const digest = createHash('sha1')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Preserve native-build identity, or validate the deliberate loss of it. Two different native payloads/upstream UUIDs currently get the same output UUID for the same app/name/CPU. Apple's TN3178 distinguishes native images for symbol matching. There is no repository crash-upload pipeline, but that does not eliminate OS crash-report or external-symbol costs. I have not reproduced an application failure; this is a follow-up tradeoff, not a P2 regression claim.

Comment thread scripts/macos-executable-uuid.mjs Outdated

function sliceOffsets(binary) {
if (binary.length >= 4 && binary.readUInt32BE(0) === FAT_MAGIC) {
const count = binary.readUInt32BE(4);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Reject empty or malformed slices before treating the UUID check as successful. Buffer.from('cafebabe00000000', 'hex') yields [], so assertMacAppExecutableUuids accepts a helper containing only this zero-slice header. The parser also accepts an LC_UUID declaring cmdsize=8 and writes past that declared command. Require a nonempty bounded fat table, bounded slices/load commands, and a full 24-byte UUID command; add negative tests. Later codesign/Gatekeeper/launch checks remain, so this is parser hardening, not a demonstrated invalid-release bypass.

sosyz and others added 3 commits October 28, 2026 22:06
electron-builder ships Electron's prebuilt executables renamed but not
relinked, so Maka's main executable and helpers carried Electron 43.4.1's
stock Mach-O LC_UUID, shared with every other app on that Electron release.
macOS Local Network Privacy identifies a program by that UUID, so another
app's grant or denial could apply to Maka.

An afterPack hook now stamps each executable, before signing, with a UUIDv5
derived from the app id, executable name and CPU type: unique to Maka,
stable across releases so a user's decision survives updates. The macOS
release verifier fails a package whose executables lack the stamp.

Generated-by: Claude Code (Claude Opus 5.5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-by: Claude Code (Claude Opus 5.5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reject a universal header with no slices or out-of-file slices, load
commands outside sizeofcmds, and an LC_UUID whose cmdsize is not 24, so
the release check can no longer pass an executable it did not read. A
golden-value test pins the derived UUID, which is now part of the shipped
identity. The rationale comment cites TN3178 and no longer claims a fixed
UUID preserves Local Network decisions; TN3179 says those follow the code
signature.

Generated-by: Claude Code (Claude Opus 5.5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sosyz
sosyz force-pushed the fix/macos-executable-uuid branch from 59e615a to 43054f8 Compare October 28, 2026 14:19

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/M Under 500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop (macOS): main executable ships Electron's stock LC_UUID, colliding with every other app on the same Electron version

2 participants