Skip to content

Refuse a macOS bundle that is not sealed around its sidecars - #31

Merged
hiroaki222 merged 4 commits into
mainfrom
ci/verify-bundle-seal
Aug 14, 2026
Merged

hiroaki222 merged 4 commits into
mainfrom
ci/verify-bundle-seal

Conversation

@hiroaki222

@hiroaki222 hiroaki222 commented Aug 14, 2026 •

Copy link
Copy Markdown
Owner

One step in the release workflow, on the job that builds the macOS app.

What it catches

The linker ad-hoc signs the executable on its own, which is not the same as
sealing the bundle around it. Without a _CodeSignature covering the sidecars,
macOS calls the app damaged and offers only the Trash — not the "open anyway"
setting the release notice walks people through.

Nothing in this workflow opens the app. It builds it, uploads it, and publishes
it, so a bundle in that state reaches the releases page looking exactly like a
good one and stays there until somebody downloads it and finds out.

How

The sidecars are required beside the executable, in Contents/MacOS, because
that is the only place the app looks: tools::locate resolves a tool against
the directory holding the running binary and otherwise falls back to a bare
name on the PATH — silently, on a machine with no ffmpeg on it. That directory
is fixed by the bundle format rather than by Tauri, so naming it is not a
guess.

Then codesign --verify --deep --strict: --deep reaches the nested
executables, --strict refuses a seal that does not match what is there.

What the drafts got wrong

Three things, each found by running the step rather than reading it:

  • It parsed codesign --verbose output looking for the sidecar's path. That
    reads a format nobody promises to keep.
  • It searched all of target and took whichever Transcrate.app came first,
    and piped find into head, which under pipefail fails the step when
    head leaves early. That needs more output than a pipe buffer holds —
    reproduced at two thousand matches, exit 141 on a bundle that is sealed.
  • It accepted a sidecar anywhere inside the bundle. A copy under
    Contents/Resources is signed, verifies, and is never opened by the app
    shipped with it. The reasoning had been that where Tauri puts an
    externalBin is Tauri's business; it is not, because tools::locate states
    where the tool has to be.

Tried

Against a bundle built for the purpose — a minimal .app with ffmpeg and
ffprobe in it, signed ad-hoc the way tauri-action signs the real one, run
under /bin/bash 3.2 as the runner has.

result
correct bundle passes
same-named folder elsewhere under target passes, checks the right one
sidecar sealed under Contents/Resources not beside the executable, and says where it is
no app built no Transcrate.app was built
sidecar outside the bundle not beside the executable
sidecar replaced after signing nested code is modified or invalid
unsigned executable added after signing a sealed resource is missing or invalid
sidecar's own signature stripped fails

The first attempt at the "replaced after signing" row removed the sidecar and
put the same bytes back, which leaves the seal matching and passes correctly.
It was rewritten to break the bundle for real.

Where it came from

This was written before the review branch and left behind on a branch cut from
an older main, where the surrounding job has since changed. It is reinstated
here rather than rebased, because the check itself was worth rewriting.

Worth having in before the next release rather than after it.

The linker ad-hoc signs the executable on its own, which is not the same as
sealing the bundle around it. Without a _CodeSignature covering the sidecars,
macOS calls the app damaged and offers only the Trash — and nothing here
opens the app, so that stays invisible until somebody downloads it.

The sidecars are found rather than looked for at a fixed path: where Tauri
puts an externalBin is Tauri's business, and a check that hard-codes it fails
on a bundle that is fine.

Tried against a bundle built for the purpose. It passes a sealed one, and
fails each of: a sidecar outside the bundle, a sidecar replaced after
signing, an unsigned executable added after signing, and a sidecar whose own
signature has been stripped.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@hiroaki222, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 90 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a178929-42b2-4dfd-a2b1-2be780fc34b3

📥 Commits

Reviewing files that changed from the base of the PR and between 7af5474 and eccc416.

📒 Files selected for processing (1)
  • .github/workflows/release.yml

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Two ways the first version could fail a build that was fine.

It searched all of target, taking whichever Transcrate.app find reached
first — so a stray one anywhere under there decided what got checked. It
searches the folder the upload step already reads.

It piped find into head, and under pipefail head leaving early sends find
SIGPIPE and fails the step. That needs more output than a pipe buffer holds,
which this search would not produce; the reason it would not is a count
nobody is watching. `-print -quit` stops at the first match without a pipe.

Both were found by running the step rather than by reading it. The SIGPIPE
case reproduces at two thousand matches, exit 141 on a bundle that is sealed.

Tried again against a bundle built for the purpose: it passes a sealed one,
ignores a same-named folder elsewhere in target, and fails each of a missing
app, a sidecar outside the bundle, a sidecar replaced after signing, an
unsigned executable added after signing, and a sidecar whose own signature
has been stripped.
The check accepted ffmpeg anywhere inside the bundle. The app does not: it
resolves a tool against the directory holding the running executable and
otherwise falls back to a bare name on the PATH, silently, on a machine that
has no ffmpeg. A copy sealed under Contents/Resources passed — signed,
verified, and never opened by the app that shipped with it.

The reasoning behind the looser check was that where Tauri puts an
externalBin is Tauri's business. It is not: tools::locate states where the
tool has to be, so that is the assumption worth holding the build to.

Contents/MacOS is that directory by the bundle format rather than by Tauri,
so naming it is not a guess. When it fires it says where the file was found
instead, which is the question anybody reading the failure will have.

Tried against a bundle built for the purpose, under /bin/bash 3.2 as the
runner has. It passes a correct bundle, and fails a sidecar sealed under
Resources, a missing app, a sidecar outside the bundle, one replaced after
signing, an unsigned executable added after signing, and one whose own
signature has been stripped.
@hiroaki222
hiroaki222 merged commit b4dd68a into main Aug 14, 2026
6 checks passed
@hiroaki222
hiroaki222 deleted the ci/verify-bundle-seal branch August 14, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant