Skip to content

unbreak macOS Apple Silicon install, atomic Linux install, real version smoke test#11

Open
staylor wants to merge 1 commit into
lightpanda-io:mainfrom
staylor:fix-install-macos-and-atomic
Open

unbreak macOS Apple Silicon install, atomic Linux install, real version smoke test#11
staylor wants to merge 1 commit into
lightpanda-io:mainfrom
staylor:fix-install-macos-and-atomic

Conversation

@staylor
Copy link
Copy Markdown

@staylor staylor commented May 5, 2026

What & why

Three fixes in scripts/install.sh. Each is independently a bug; grouped because they all sit in the same script and #1 motivates the diagnostic improvements in #2 and #3.

1. macOS Apple Silicon install produces a binary the kernel kills at exec

The nightly Mach-O lightpanda-aarch64-macos gets SIGKILLed at exec when run from arbitrary user paths (/tmp, ~/.local/bin/, etc.) on Apple Silicon. The kernel log line is:

AMFI: '...lightpanda-aarch64-macos' has no CMS blob?
AMFI: '...lightpanda-aarch64-macos': Unrecoverable CT signature issue, bailing out.

Root cause

The Mach-O's codesign -dv shows flags=0x20002 (adhoc, linker-signed) — the build linker emitted a minimal ad-hoc signature with a CodeDirectory and page hashes but no CMS (Cryptographic Message Syntax) blob. AMFI on Apple Silicon enforces stricter requirements on ad-hoc signatures outside certain trusted prefixes; without the CMS blob, AMFI rejects the binary at exec from arbitrary paths.

I verified empirically:

  • Same byte-identical Mach-O (SHA-256 f7acdfdd...) runs fine from /opt/homebrew/Cellar/lightpanda/.../bin/lightpanda, gets SIGKILLed from /tmp/lightpanda-aarch64-macos.
  • Copying the broken curl-downloaded bytes into /opt/homebrew/Cellar/... makes them run.
  • Re-signing locally with codesign -s - adds a CMS blob but AMFI then rejects with error -423 "adhoc signed or signed by an unknown certificate chain".

So ad-hoc re-signing genuinely doesn't help; the actual fix is either (a) upstream-side proper signing or (b) installing to a path AMFI exempts.

What this PR does

Routes Apple Silicon only through brew install lightpanda-io/browser/lightpanda. The brew formula's bin.install step places the same byte-identical Mach-O at /opt/homebrew/Cellar/lightpanda/.../bin/lightpanda, an AMFI-exempt path, where it runs.

Important honesty note: I previously assumed the brew tap built from source. It does not. The formula is:

on_macos do
  if Hardware::CPU.arm?
    url ".../releases/download/nightly/lightpanda-aarch64-macos"
    sha256 "f7acdfdd..."
  ...
def install
  bin.install Dir["lightpanda-*"].first => "lightpanda"
end

The "fix" is purely about install path, not about how the binary was produced. The install script is using brew as a way to land the binary at an AMFI-exempt location.

Better long-term fix (out of scope for this PR)

The proper fix is upstream-side: the build pipeline that produces the GitHub Release assets should apply a proper ad-hoc signature with a CMS blob (or, better, a Developer ID signature + notarization). Then the release Mach-O would run from anywhere and this brew workaround wouldn't be needed.

Intel macOS

Left on the GitHub Release Mach-O path. I don't have an Intel Mac to test, but Apple Silicon's stricter AMFI signature enforcement is a known arm64-specific behavior (the kernel signature path was tightened around macOS 11 / Apple Silicon transition); the same Mach-O may well run fine from ~/.local/bin/ on Intel. Happy to widen the brew routing if you have data showing Intel needs it too.

Test environment

Reproducing on M3 Pro / macOS 14.x. The kernel-signing path is uniform across Apple Silicon hardware, so this should affect all M-series Macs, but I haven't directly tested M1/M2/M4 or older macOS versions.

2. Linux (and Intel macOS) install destroys an existing working binary on any failed rerun

SKILL.md documents reruns of this script as the update path:

If you encounter crashes or issues, run scripts/install.sh again to update to the latest version (max once per day).

But curl -L -o "$INSTALL_DIR/$BINARY_NAME" overwrites the install target during download. A failed download (network drop, checksum mismatch, unrunnable replacement) leaves the user with no binary or a broken one — even though they had a working one before.

This PR downloads to a mktemp file in the same directory, verifies checksum + smoke-tests, then atomically mvs into place. The existing binary is preserved on any failure.

3. The --version smoke test silently accepts broken binaries

Current test:

"$INSTALL_DIR/$BINARY_NAME" --version 2>/dev/null || \
  "$INSTALL_DIR/$BINARY_NAME" --help 2>/dev/null | head -1

Lightpanda's CLI doesn't have a --version flag. Running it logs $msg=exit err=UnknownCommand to stderr and exits 1. The 2>/dev/null masks this and the script falls through to the --help branch. On a binary that crashes at exec, --help also fails, but the surrounding if ... | head -1; then construction still ends in the success branch because head exits 0 on empty stdin. Result: Lightpanda installed successfully! for an unrunnable binary.

This PR uses the actual version subcommand, captures stderr to a tempfile, and fails hard with the binary's stderr surfaced so users see real diagnostics like GLIBC_2.32 not found on incompatible systems.

Live reproduction of the broken --version:

$ /opt/homebrew/bin/lightpanda --version
$msg=exit err=UnknownCommand
$ echo $?
1
$ /opt/homebrew/bin/lightpanda version
1.0.0-nightly.6051+d360fcc0
$ echo $?
0

Smaller adjacent changes

These are tightly motivated by the three fixes above:

  • set -euo pipefail (was set -e). pipefail propagates a failing curl through curl | jq (jq exits 0 on empty input, otherwise hiding network errors behind a misleading "could not retrieve checksum"). curl -fSL (was -sL/-L) makes curl exit non-zero on HTTP errors so set -e catches them.
  • chmod 0755 instead of chmod a+x. Only relevant because of fix update metadata and move install into scripts dir #2's mktemp, which creates 0600; a+x on top of that yields 0711 (owner-only readable).
  • Apple Silicon PATH-shadow check. Without this warning, a user upgrading from a pre-this-PR version still has the rejected ~/.local/bin/lightpanda (the linker-signed Mach-O) which shadows the new working brew binary in PATH — so they'd run lightpanda and still get SIGKILL even though the brew install succeeded.
  • LIGHTPANDA_DIR warning on Apple Silicon (brew owns the install path; the variable is silently ignored without this).
  • SKILL.md Install section gains a per-OS callout describing the new flow split (Apple Silicon → brew, Intel + Linux → release Mach-O).

Out of scope (intentionally not included)

  • Optional $GITHUB_TOKEN to raise the GitHub API quota from 60→5000/hr.
  • PATH-shadow check for the shared download flow (Linux + Intel macOS).
  • An upstream issue / fix on lightpanda-io/browser for the linker-signed Mach-O. That's the real long-term solution.

Verified

End-to-end run on Apple Silicon: brew install/upgrade succeeds, version subcommand prints 1.0.0-nightly.6051+d360fcc0, PATH-shadow warning fires when a stale binary is present, LIGHTPANDA_DIR warning fires when the variable is set. Smoke-test stderr capture exercised by deliberately corrupting the temp binary. The byte-identical-bytes-in-different-paths claim verified by direct test (broken curl bytes → AMFI-exempt brew path → runs; working brew bytes → /tmp → SIGKILL).

Intel macOS path is structurally identical to the Linux flow — same shared download/verify/smoke-test/mv code. Linux is what the existing script's tests have always exercised.

Stack

Independent of #12 (docs corrections including #10 point 2) and #13 (--block-private-networks defaults). All three branch off main directly.

@staylor staylor force-pushed the fix-install-macos-and-atomic branch from 400500e to b0501d6 Compare May 5, 2026 14:46
@staylor staylor changed the title fix(install): unbreak macOS, atomic Linux install, real version smoke test unbreak macOS Apple Silicon install, atomic Linux install, real version smoke test May 5, 2026
@staylor staylor force-pushed the fix-install-macos-and-atomic branch 4 times, most recently from d99615d to 6fcd30f Compare May 5, 2026 15:14
…on smoke test

Three independently-motivated bug fixes in scripts/install.sh:

1. macOS Apple Silicon: AMFI rejects the release Mach-O at exec
   The lightpanda-aarch64-macos asset has a linker-signed ad-hoc
   signature without a CMS blob (`codesign -dv` shows
   flags=0x20002(adhoc,linker-signed)). AMFI on Apple Silicon
   enforces stricter requirements outside trusted prefixes; from
   /tmp or ~/.local/bin the kernel rejects it as
   'Unrecoverable CT signature issue, bailing out' and SIGKILLs
   at exec. Re-signing locally with `codesign -s -` adds a CMS
   blob but AMFI then rejects with error -423 (adhoc signed by
   unknown chain).

   Verified empirically: same byte-identical Mach-O runs from
   /opt/homebrew/Cellar/lightpanda/.../bin/lightpanda but is
   SIGKILLed from /tmp/. Copying the broken curl-downloaded
   bytes INTO the brew path makes them run. So the trust is
   path-based, not byte-based.

   The Lightpanda brew formula does NOT build from source
   (verified via `brew cat`) — it just downloads the same
   Mach-O and runs `bin.install` to place it at
   /opt/homebrew/Cellar/.../bin/lightpanda, which AMFI exempts
   from the strict ad-hoc signature check.

   Reproducing on M3 Pro / macOS 14.x; should be uniform across
   Apple Silicon but not directly tested on M1/M2/M4. Intel
   macOS keeps the GitHub Release Mach-O path — Apple Silicon's
   stricter AMFI enforcement is arm64-specific and I don't have
   an Intel Mac to test.

2. Failed reruns destroy existing working binary
   SKILL.md documents reruns as the update path, but
   curl -L -o "$INSTALL_DIR/$BINARY_NAME" overwrites the install
   target during download. Atomic install via mktemp + verify +
   smoke-test + mv preserves the existing binary on any failure.

3. --version smoke test silently accepts broken binaries
   Lightpanda's CLI has no --version flag; running it logs
   '$msg=exit err=UnknownCommand' and exits 1. The current
   2>/dev/null + --help fallback masks this, and the head -1 pipe
   at the end means it succeeds on broken binaries that produce
   no output. Use the 'version' subcommand and capture stderr
   to surface real diagnostics like 'GLIBC_2.32 not found' on
   failure.

Smaller adjacent changes, each tightly motivated by the three above:

- set -euo pipefail (was set -e); curl -fSL (was -sL/-L). pipefail
  + -f propagate curl HTTP errors through curl | jq, replacing the
  misleading 'could not retrieve checksum' message.
- chmod 0755 instead of chmod a+x. Required because fix lightpanda-io#2 uses
  mktemp (0600); chmod a+x on top yields 0711 (owner-only readable).
- Apple Silicon PATH-shadow warning. A user upgrading from a pre-
  this-PR version still has the rejected ~/.local/bin/lightpanda
  (the linker-signed Mach-O) which shadows the new working brew
  binary in PATH — without the warning they still get SIGKILL after
  a successful brew install.
- LIGHTPANDA_DIR warning on Apple Silicon (brew owns the install
  path; the variable is silently ignored without this).
- SKILL.md Install section: per-OS callout describing the flow split.

Better long-term fix (out of scope for this PR): upstream-side, the
build pipeline producing the GitHub Release assets should apply a
proper ad-hoc signature with a CMS blob (or, better, Developer ID +
notarization). The release Mach-O would then run from anywhere and
this workaround wouldn't be needed.

Intentionally not included (offered as follow-ups):
- Optional $GITHUB_TOKEN for API quota
- PATH-shadow check on the shared download flow
- Upstream-side signing fix (separate repo)
@staylor staylor force-pushed the fix-install-macos-and-atomic branch from 6fcd30f to ef4e6cd Compare May 5, 2026 15:25
@staylor staylor marked this pull request as ready for review May 5, 2026 15:29
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