Fix undefined 32-bit shift that traps on arm64 (daemon crashes on card read) - #1256
Open
hyperbotsx wants to merge 2 commits into
Open
Fix undefined 32-bit shift that traps on arm64 (daemon crashes on card read)#1256hyperbotsx wants to merge 2 commits into
hyperbotsx wants to merge 2 commits into
Conversation
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.
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.
limpkin
reviewed
Jul 27, 2026
| // arm64 the compiler turns it into a trap (crash on Apple Silicon). | ||
| for (int i = 0;i < std::min(8, static_cast<int>(cardCPZ.size()));i++) | ||
| m_key += (static_cast<unsigned int>(cardCPZ[i]) & 0xFF) << (i * 8); | ||
| m_key += (static_cast<unsigned int>(cardCPZ[i]) & 0xFF) << ((i * 8) & 31); |
Collaborator
There was a problem hiding this comment.
(unless I'm mistaken) and that's why AI isn't there yet: for the same input, this change will lead to different m_key outputs as nothing will be populated after the 32th bit.
https://github.com/mooltipass/minible/wiki/Mooltipass-Database-Model#-cpz-lookup-table
limpkin
requested changes
Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What breaks
When Moolticute is compiled natively for Apple Silicon (arm64),
moolticutedcrashes every time a card CPZ is read — i.e. on every device connect/unlock, which makes the app unusable. Crash reports from a Mooltipass Mini BLE on macOS 26.5.2 (M-series Mac) are deterministic:Why
FilesCache::setCardCPZ()andMPDevice::getUInt64EncryptionKeyOld()derive the SimpleCrypt key from the 8-byte card CPZ like this:The shifted operand is 32-bit, so for
i >= 4this shifts by 32..56 — undefined behaviour. x86 silently wraps the shift count modulo 32, which is what every historical Intel build actually computed. For arm64, clang proves those iterations are undefined and emits a trap instead: disassembling the built binary showsbrk #1(0xd4200020) at exactly the crash offset insideFilesCache::setCardCPZ.The unit tests don't catch it — none exercise the CPZ path, so the suite passes 63/63 on an arm64 CI runner while the app crashes against real hardware.
Note that
MPDevice::getUInt64EncryptionKey()(theSIMPLE_CRYPT_V2path) already does this correctly in 64-bit; only the legacy path and theFilesCachecopy were left with the undefined shift.The fix
Mask the shift count (
(i * 8) & 31) so the historical x86 wrapping becomes explicit and well-defined. Derived keys stay byte-identical to what existing Intel installs produce, so cached data and SimpleCrypt-encrypted exports continue to decrypt — no migration and no data loss. The V2 path is untouched.The second commit is related and was found alongside it:
_read_report_callback()insrc/MPDevice_mac.cppignored theIOReturn resultargument. IOKit invokes that callback with an error result (and a meaningless buffer/length) when a device drops mid-read — for example while a BLE device re-enumerates its interfaces — and those bytes were parsed as if they were a real device message. Errored reports are now discarded.Verification
Built on a
macos-14(arm64) runner and tested on hardware (Mooltipass Mini BLE, macOS 26.5.2):mc-cli, notes read/write, device settings change/revert, and unplug/replug recovery all pass. A binary check confirms nobrkremains inFilesCache::setCardCPZ.Related to #1254 and #1255 (native Apple Silicon builds), but this fix is independent of any build-pipeline change — it is a latent correctness bug in the codebase that only manifests once the code is compiled for arm64.