ReviewOS

craft-native/craft

fix(release): standalone macOS CLI crashes during App Sandbox initialization

#282
Closed glennmichael123 opened this 10 days ago ·
10 days ago

Problem

Both macOS binaries published in v0.0.92 fail strict code-signature verification after downloading and extracting the exact GitHub release assets.

Release: https://github.com/craft-native/craft/releases/tag/v0.0.92

Reproduction

gh release download v0.0.92 --pattern "craft-darwin-*.zip"
unzip craft-darwin-arm64.zip -d arm64
unzip craft-darwin-x64.zip -d x64
codesign --verify --deep --strict --verbose=4 arm64/craft
codesign --verify --deep --strict --verbose=4 x64/craft

Both report:

invalid signature (code or signature have been modified)

The arm64 binary exits 134 when invoked with --version on an Apple Silicon host. Signature metadata still contains TeamIdentifier 3JJRNQW6B7 and hardened-runtime flags, but the Authority is unavailable and the sealed code no longer verifies.

The downloaded bytes match release-manifest.json exactly:

  • arm64 SHA-256 bb13ba8af990d686480df6d082bad6a26cf9979867219b47429d3e00dffd1249, size 4558116
  • x64 SHA-256 b51fae5d4fd69f8f24192e6cbb8215aeb0a3057d05e6cf8dfe3e8c4b4bb32dda, size 5052897

Likely release-path gap

release.yml verifies each binary immediately after signing and notarizes a temporary archive, but the Pantry Publish & Release action subsequently creates the distributed archives. Any transformation or replacement between those steps is not verified. The release asset gate checks only asset names, not signatures or executable behavior.

Expected / acceptance criteria

  • Determine what changes or replaces the Mach-O after codesign.
  • Preserve valid Developer ID signatures through packaging for both architectures.
  • Download the final release archives in CI, extract them, run codesign --verify --deep --strict, and execute craft --version on arm64 (plus x64 where available).
  • Verify the final downloadable artifacts, not only pre-publication workspace files.
  • Do not cut the next release until the final-asset check is green.

Discovered during the post-merge release audit for #254, #242, and #220. Related manifest completeness issue: #281.

10 days ago

Narrowing evidence:

  • v0.0.91 and v0.0.90 arm64 release binaries both pass codesign --verify --deep --strict and expose the full Developer ID certificate chain.
  • v0.0.92 is the first checked regression; its arm64 and x64 binaries both fail.
  • v0.0.92 release job 35240362950 shows SIGNING_AVAILABLE=true, successful strict verification after signing, and Apple notarization status Accepted for submission IDs a69c5f0-7849-4fb0-8703-c69e6ea and 4c332c0-1126-4aaa-a3db-e2c121b.
  • The next step is the Pantry Publish & Release action. Its log packages zig-out/bin/craft and zig-out/cross/darwin-x64/craft with /usr/bin/zip, then uploads them. There are no retained pre-publication workflow artifacts to compare.
  • The v0.0.92 arm64 binary has a structurally present embedded signature (LC_CODE_SIGNATURE dataoff 14138528, datasize 47792); the CMS digest equals the candidate CodeDirectory hash, but page verification fails. This strongly indicates code bytes changed after the final successful pre-publication verification and before/during the Pantry packaging action.

The executable test was also repeated with the native craft version command and exits 134 with no version output on Apple Silicon.

10 days ago

Correction to the prior narrowing note: repeat verification of v0.0.90 and v0.0.91 later in the same session also changed to invalid signature, with Authority=(unavailable), even though their bytes did not change. A zip round-trip of v0.0.91 produced byte-for-byte identical output (same SHA-256) yet the later trust result failed. Do not treat v0.0.92 as a proven first regression or Pantry ZIP as a proven byte mutation.

The durable facts are:

  • Current verification fails for the downloaded v0.0.90, v0.0.91, and v0.0.92 macOS binaries.
  • The embedded CMS digest still equals each candidate CodeDirectory hash, but the certificate authority becomes unavailable.
  • The v0.0.92 binaries were accepted by Apple notarization and passed verification during the release job, while they fail now.
  • codesign -dvvv shows no timestamp line. This points toward certificate/trust/timestamp validation as another strong possibility, not just post-sign byte mutation.

Acceptance should therefore retain final-asset verification and also verify secure timestamps/certificate validity over time. The blocker remains valid, but the root cause needs certificate-chain/timestamp investigation before changing packaging.

10 days ago

Deeper cryptographic check rules out byte mutation for the downloaded v0.0.92 arm64 binary:

  • Parsed the embedded CodeDirectory: SHA-256, 16 KiB pages, 863 code slots, codeLimit 14,138,528. Recomputed every slot hash from the downloaded Mach-O: 0 mismatches.
  • Extracted the CMS blob and verified its detached signature over the exact CodeDirectory with OpenSSL: CMS Verification successful.
  • The embedded Developer ID leaf is valid 2026-08-30 through 2031-08-31; its G2 intermediate is valid through 2031; Apple Root CA through 2035.
  • The CMS has signingTime 2026-09-17 15:41:20 GMT and an RFC 3161 timestamp token for the same time.

So the artifact bytes, CodeDirectory hashes, CMS signature, certificate dates, and timestamp are internally coherent. The failure is now narrowed to macOS trust-policy evaluation (Authority=(unavailable), Security.framework OSStatus -26276), not ZIP mutation. It reproduces on macOS 26.3.1 (25D771280a), where launch also fails. The release gate should test the final asset on supported macOS versions and capture Security.framework/notarization diagnostics; blindly re-signing or replacing ZIP alone is unlikely to fix it.

10 days ago

Additional independent verification on the shipped x86_64 macOS asset matches the arm64 result: recomputing all 3,786 SHA-256 CodeDirectory page slots at the declared 4 KiB page size produced 0 mismatches, and the detached CMS signature verifies successfully with OpenSSL. Together with the earlier arm64 result (863/863 matching 16 KiB page slots), this rules out ZIP/download mutation for both architectures and keeps the failure narrowed to Apple Security.framework trust-policy evaluation on the current macOS 26.3.1 host.

10 days ago

The signature error was caused by the local execution sandbox blocking macOS trust services. Repeating codesign --verify --deep --strict outside that sandbox passes for both exact v0.0.92 release binaries; their Developer ID chain and secure timestamp are present. The earlier conclusion that the published signatures are invalid is withdrawn.

There is a separate, confirmed launch defect: the arm64 release executable traps before main in _libsecinit_appsandbox. The crash diagnostic says Unable to get bundle identifier ... Info.plist from code signature information has no value for kCFBundleIdentifierKey. The raw CLI is signed with com.apple.security.app-sandbox=true despite having no embedded bundle identifier.

Matched control experiment using copies of the exact release executable:

  • Ad-hoc sign with hardened runtime and the existing distribution entitlements: exits 133 (SIGTRAP).
  • Ad-hoc sign with hardened runtime and only the JIT entitlement: exits 0 and prints craft version 0.0.92.

This isolates the App Sandbox entitlement as the launch cause. The fix will give the standalone Pantry CLI suitable entitlements while retaining hardened runtime and library validation; packaged application sandbox policy remains separately configured. Regression coverage will execute a signed standalone fixture and verify final downloaded release assets, in addition to pre-publication signing checks.

10 days ago

Reproduced against the exact published bytes on an Apple Silicon host. Both archives hash as recorded, so this is the same artifact the report tested:

bb13ba8af990d686480df6d082bad6a26cf9979867219b47429d3e00dffd1249  craft-darwin-arm64.zip
b51fae5d4fd69f8f24192e6cbb8215aeb0a3057d05e6cf8dfe3e8c4b4bb32dda  craft-darwin-x64.zip

The signature is intact — that part of the diagnosis is wrong

codesign --verify --deep --strict --verbose=4 passes here for both architectures:

arm64/craft: valid on disk
arm64/craft: satisfies its Designated Requirement

The full chain resolves, with a secure timestamp and hardened runtime:

Authority=Developer ID Application: Chris Breuer (3JJRNQW6B7)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Sep 17, 2026 at 11:41:20 PM
CodeDirectory v=20500 flags=0x10000(runtime)

Same bytes, opposite verdict, so invalid signature (code or signature have been modified) was specific to the verifying machine — consistent with the report's own note that "the Authority is unavailable", i.e. the Developer ID CA could not be resolved there. Nothing transforms or replaces the Mach-O after codesign. The "likely release-path gap" section can be dropped; it would cost the next reader a long hunt.

The actual cause is the entitlements

The v0.0.92 binaries are signed with:

<key>com.apple.security.app-sandbox</key><true/>
<key>com.apple.security.files.user-selected.read-write</key><true/>
<key>com.apple.security.network.client</key><true/>

App Sandbox on a raw executable with no embedded Info.plist traps in libsecinit before main. I measured exit 133 (SIGTRAP) rather than the reported 134, but it dies at startup with no output either way. The other two keys are sandbox-only and inert without it.

Decisive test — the same bytes, signature removed and re-signed ad-hoc with --options runtime and no entitlements:

$ ./probe --version
craft version 0.0.92
Built with Zig 0.17.0-dev
Platform: macOS
exit=0

So the executable is fine; only the entitlement set killed it.

Already fixed for future releases

scripts/entitlements-distribution.plist now carries com.apple.security.cs.allow-jit alone, and documents this exact trap ("enabling App Sandbox here traps in libsecinit before main"). The v0.0.92 assets predate it — they were signed on 17 September.

The final-asset gate the acceptance criteria ask for also exists: verify-macos-downloads downloads the published archive on macos-15 and macos-15-intel, extracts with ditto, and runs scripts/verify-macos-release.ts, which inspects entitlements and executes the binary to compare craft version <tag>.

One thing worth keeping in mind

codesign --verify --deep --strict would never have caught this — it passes on the broken binary. The in-line release checks at release.yml:275 and :308 are signature-only, so the protection rests entirely on the step that runs the executable. Worth not reducing that to a signature check later.

Suggest leaving this open until the next release shows verify-macos-downloads green on both legs, since the v0.0.92 assets remain broken and immutable.

10 days ago

Verified the immutable v0.0.98 downloadable assets, not just pre-publication files. craft-darwin-arm64.zip and craft-darwin-x64.zip match release-manifest.json SHA-256 values (9b6f2045…914c8736 and 4bb130b7…f756d66b4f). After extraction on Apple Silicon, bun scripts/verify-macos-release.ts <binary> 0.0.98 passed strict codesign verification, Developer ID chain/timestamp/hardened-runtime and entitlement checks, and executed both arm64 and x64 binaries successfully as craft version 0.0.98. The release workflow's native verify downloaded darwin-arm64 and verify downloaded darwin-x64 jobs also passed: https://github.com/craft-native/craft/actions/runs/36445986425 . The standalone CLI App Sandbox regression is fixed in a published release, so closing this issue.

Sign in to comment on this issue.