Skip to content

Hardware validation, CI hardening and a critical arm64 crash fix for the Apple Silicon build - #1

Open
hyperbotsx wants to merge 13 commits into
poitee:feature/apple-silicon-macosfrom
hyperbotsx:feature/apple-silicon-native
Open

Hardware validation, CI hardening and a critical arm64 crash fix for the Apple Silicon build#1
hyperbotsx wants to merge 13 commits into
poitee:feature/apple-silicon-macosfrom
hyperbotsx:feature/apple-silicon-native

Conversation

@hyperbotsx

Copy link
Copy Markdown

First of all — thank you for mooltipass#1255, it was an excellent base. I took your branch, hardened the pipeline, and validated the result end-to-end on real hardware (Mooltipass Mini BLE, macOS 26.5.2, M-series Mac). This PR contributes those changes back so they can be reviewed together with your work. Everything is small, separable commits — feel free to cherry-pick.

The critical finding: native arm64 builds crash on card read

Testing with a real device immediately hit a daemon crash-loop on every connect/unlock: EXC_BREAKPOINT (SIGTRAP) at FilesCache::setCardCPZ+1376, reached from the IOHID input-report path via MPDevice::getCurrentCardCPZ().

Root cause (aae9bf58): FilesCache::setCardCPZ and MPDevice::getUInt64EncryptionKeyOld derive a SimpleCrypt key with (32-bit value) << (i*8) for i = 0..7. Shifting a 32-bit type by ≥ 32 is undefined behaviour — x86 silently wraps the shift count, but clang for arm64 compiles the provably-undefined iterations into a brk trap. I confirmed the shipped binary literally contains brk #1 (0xd4200020) at the crash offset. So every native Apple Silicon build of master crashes the moment a card CPZ is read — unit tests can't see it (none exercise that path).

The fix masks the shift count ((i*8) & 31), making the historical x86 wrapping explicit and well-defined. Derived keys stay byte-identical with existing Intel installs, so caches and SimpleCrypt-encrypted exports keep decrypting. (The SIMPLE_CRYPT_V2 path already shifts in 64-bit and is untouched.) After this fix, unlock and all device operations work — verified on hardware.

A companion hardening (4454a2ab): the macOS HID input-report callback ignored the IOReturn result, parsing garbage when a device drops mid-read (e.g. BLE interface re-enumeration). Errored reports are now dropped.

Pipeline hardening (on top of your workflows/scripts)

  • 143ba5e4 fix relative-dest bug in build_mc_cli_tools (go build ran from inside the clone, so after_success.sh's relative path would misplace the binaries)
  • 02fa9263 pin mc-agent/mc-cli to known-good revisions instead of floating master (reproducibility/supply chain; both repos are dormant)
  • c3fe6d67 fetch-depth: 0 in macos-build.yml — make_version needs git describe --tags, which dies in a shallow clone
  • 9f443ec9 workflow_dispatch, feature/apple-silicon-* branch trigger, build with --package, upload an installable .app artifact from every build run
  • 0c47c67f CI now asserts all four executables (moolticute, moolticuted, mc-agent, mc-cli) are exactly arm64 instead of just printing lipo output
  • c4a35ca8 workflows install pinned official Qt 6.2.4 via aqtinstall (same version upstream Travis uses; official archives are universal, which keeps a future universal-binary build one variable away). Homebrew Qt remains fully supported for local builds; detect_qtdir also checks the qt@6 keg paths now
  • a15ea400 pin Go via actions/setup-go
  • 58153ebf codesign: fail loudly instead of 2>/dev/null || true (a silent failure would ship a DMG that arm64 macOS refuses to launch), sign nested binaries explicitly, verify with codesign --verify --strict
  • 0b52b2a2 retry hdiutil create (intermittent "Resource busy" on GH runners)
  • a9cf5cc3 stamp CFBundleShortVersionString/CFBundleVersion at package time and declare LSMinimumSystemVersion 11.0
  • df5894c5 document two upgrade traps found in testing (stale shared-memory daemon record from a previous install; auto-updater offering the Intel v1.04.0 DMG to native builds)

Hardware validation results (macOS 26.5.2, Mac14,6, Mooltipass Mini BLE)

All over USB, on the build produced by these workflows:

Test Result
Native arch (file/lipo/Activity Monitor "Apple") for all 4 binaries
Device detection, firmware/serial display
Unlock via PIN ✅ (crash-looped 100% before the fix)
Credential read + write (persists across replug)
Browser extension login via daemon WebSocket
mc-cli (arm64, compiled from source in CI)
Notes read/write, device settings change/revert
Unplug/replug recovery
Unit tests in CI (arm64 runner) ✅ 63/63
Bluetooth/BLE ⏳ pending (device battery died mid-session — will follow up)

Also verified: the codebase builds green (incl. all tests) against Qt 6.8 LTS with zero code changes — available as an optional bump if wanted, since 6.2.4 is EOL.

Known remaining items (maintainer-side / follow-ups)

  • Developer ID signing + notarization in the workflow (test builds are ad-hoc signed → right-click-Open)
  • Auto-updater offers the Intel v1.04.0 DMG to native builds (a universal binary release would solve this cleanly — the aqt Qt in this PR is already universal-capable)
  • BLE hardware test still outstanding (as in Add native Apple Silicon macOS build support mooltipass/moolticute#1255)

CI runs: build https://github.com/hyperbotsx/moolticute/actions/runs/30150368234 · release https://github.com/hyperbotsx/moolticute/actions/runs/30153970614

🤖 Generated with Claude Code

hyperbotsx and others added 13 commits July 25, 2026 10:58
go build runs from inside the temp clone, so a relative dest (as passed
by after_success.sh) would drop the binaries in the clone instead of the
app bundle and fail the [ -f ] check. Absolutize dest on entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bundled Go CLI tools were cloned at floating master, so a push to
either repo would silently change what ships in the bundle. Pin to the
current master tips (mc-agent 2022-12-26, mc-cli 2023-08-21 - both
repos are dormant) and make the refs overridable via environment.
Dependency integrity below the pins comes from each repo's go.sum.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
make_version runs git describe --tags --abbrev=0, which fails hard in
the default shallow checkout (depth 1, no tags), killing the build
before compilation starts. The release workflow already fetches full
history; do the same here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- workflow_dispatch so any branch can be built on demand
- widen the push trigger to feature/apple-silicon-* branches
- build with --package so every run produces a complete bundle
  (daemon, CLI tools, Qt frameworks via macdeployqt)
- upload the zipped .app so PR reviewers can download and try a build
  without cutting a release

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The verify steps only printed file/lipo output, so an accidental x86_64
binary would pass CI unnoticed. Fail the run unless moolticute,
moolticuted, mc-agent and mc-cli are all exactly arm64.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Floating 'brew install qt' drifts to whatever Qt is current (untested
against this codebase) and its single-arch bottles cannot produce a
universal binary later. CI now installs the official Qt 6.2.4 archives
(exactly what the upstream Travis pipeline builds against; universal
x86_64+arm64) via aqtinstall, with qtwebsockets as the only addon
module needed. Homebrew Qt remains fully supported for local builds
through detect_qtdir, which now also checks the qt@6 keg paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces floating 'brew install go' so the compiler that builds the
bundled mc-agent/mc-cli is deterministic across runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ad-hoc signing step ended in '2>/dev/null || true', so a signing
failure would silently ship a DMG whose app macOS kills at launch
(arm64 requires at least an ad-hoc signature). Sign moolticuted and
the CLI tools explicitly before the outer bundle, drop the error
suppression, and verify the result with codesign --verify --strict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hdiutil intermittently fails with 'Resource busy' on GitHub-hosted
macOS runners; retry up to 3 times before giving up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The template Info.plist shipped without CFBundleShortVersionString,
CFBundleVersion or LSMinimumSystemVersion, so the packaged app showed
no version in Finder/Get Info and made no OS requirement claim. Stamp
the git-derived version at package time and declare macOS 11.0 (the
first arm64 release) as the minimum.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FilesCache::setCardCPZ and MPDevice::getUInt64EncryptionKeyOld derive a
SimpleCrypt key by shifting a 32-bit value by (i*8) for i in 0..7. A
shift >= 32 is undefined behaviour: x86 silently wrapped the count, but
for arm64 clang compiles the provably-undefined iterations into a brk
trap, so the daemon crashes (EXC_BREAKPOINT) the moment a card CPZ is
read - i.e. on every device connect/unlock on Apple Silicon.

Verified on hardware (Mooltipass Mini BLE, macOS 26.5.2, Mac14,6):
deterministic SIGTRAP at FilesCache::setCardCPZ+1376 in every crash
report, and the shipped binary contains brk mooltipass#1 (0xd4200020) at exactly
that offset.

Mask the shift count ((i*8) & 31) to make the historical x86 wrapping
explicit and well-defined: derived keys stay byte-identical with
existing Intel installs, so cached/exported data continues to decrypt.
The V2 path (getUInt64EncryptionKey) already shifts in 64-bit and is
untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IOKit invokes the input-report callback with a non-success result (and
meaningless buffer/length) when the device drops mid-read, such as
during BLE device interface re-enumeration. The callback ignored the
result code and parsed whatever was in the buffer. Drop such reports
instead of feeding them to the message protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Real-device testing on Apple Silicon surfaced two upgrade traps: a
stale shared-memory daemon record from the previous install blocks the
new daemon from starting, and the auto-updater offers the Intel v1.04.0
DMG to native builds. Document both with workarounds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df5894c5b4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +63 to +64
BUNDLE_VERSION="$(get_version "$REPO_ROOT" 2>/dev/null || echo v0.0.0)"
BUNDLE_VERSION="${BUNDLE_VERSION#v}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the requested release version in bundle metadata

When macos-release.yml is started via workflow_dispatch, the requested version is passed only to package-release.sh, while this new metadata stamping still reads the nearest existing Git tag. For example, dispatching v1.05.0-arm64 from a commit whose latest tag is v1.04.0 publishes v1.05-named assets containing an app whose CFBundleShortVersionString and CFBundleVersion both report 1.04.0. Pass the workflow's release version into the packaging/stamping step so the bundle and published release agree.

Useful? React with 👍 / 👎.

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